diff --git a/.cursor/rules/agent-core.mdc b/.cursor/rules/agent-core.mdc new file mode 100644 index 0000000..07790ce --- /dev/null +++ b/.cursor/rules/agent-core.mdc @@ -0,0 +1,44 @@ +--- +description: 对话、查证、改动、引用与指令优先级(本仓库默认) +alwaysApply: true +--- + +# 用户级规则(核心) + +## 非琐碎开发(回复最前 3 行) + +`Skills:`(0~3 个,无则写无)| `MCP:`(无则写无)| `操作:` 勾选 Skills 或写「跳过」。 +任务相关须读对应 `SKILL.md` 并按其执行;MCP 先读 schema,强相关再用,勿为调用而调用。 + +## 原则 + +- **不知则停**:无法从仓库或权威来源验证 → 只问缺什么或给最短验证;禁止猜、禁止先改再说。 +- **禁幻觉**:不编造路径、符号、API、配置、版本;不确定写「不确定」及如何查证。 +- **继承上下文**:结合整段对话;最新一条默认延续当前任务,区分细化与换方向。 +- **回复**:简体中文;标识符与代码保持原文。结构:**结论** → **要点** → **命令**(若有)。少寒暄、少加粗;禁「保证/一定/完美/彻底解决」。 +- **引用代码**:独占一行 `startLine:endLine:filepath`,行号与路径须真实。 +- **中文 UTF-8**:含中文文件 UTF-8 无 BOM;灌库只用 `scripts/apply-mysql-utf8.ps1`(禁 `type`/管道灌库);`bat` 不写中文 SQL。 + +## 改动 + +- **最小 scope**:只改任务所需;少动无关文件;禁止为省事堆进单文件(巨型组件、全局聚合 types/index 等)。 +- **单点推进**:同一问题优先一处修;连续两次仍不稳 → 停加代码,列 1~3 条假设或要最小复现。 +- **未要求不扩**:连带风险一句点出;与任务冲突时让用户选,不绕边界扩 scope。 +- **随改随清**:替换/迁路径/删功能时,在同一边界内删失效代码、引用、配置与文件;先搜索确认无引用再删,无法确认则问用户。 +- **清理范围**:不做无关全库大扫除;不主动删跨模块资产、文档、迁移、构建产物或用途不明的历史文件;删/留须在回复中简要说明。 + +## 结构 + +- 动手前读仓库既有目录、命名与同类模块(`README` / `AGENTS.md` / `docs` / 邻近代码),新代码跟现有一致。 +- **一次一边界**(feature / 层 / 子系统);契约(类型、API、路由配置)与实现(handler、service、UI)分离,避免逻辑散落。 +- 单文件过大先按仓库习惯拆分;复用现有抽象;diff 与 UI/错误处理跟仓库一致。 + +## 执行 + +- 能本机执行则直接执行并回报;不把可执行项只写成教程;失败可换路径再试。 +- `user_info` 中的日期/年份以该字段为准。 +- 密钥、令牌、生产配置脱敏,不写入仓库与日志。 + +## 遵循 + +- 用户规则、工具说明、Skill 说明中指定格式/流程须执行,非建议。 diff --git a/.cursor/rules/coding-style.mdc b/.cursor/rules/coding-style.mdc new file mode 100644 index 0000000..75da8d5 --- /dev/null +++ b/.cursor/rules/coding-style.mdc @@ -0,0 +1,12 @@ +--- +description: 写代码时的约束(本仓库默认) +alwaysApply: true +--- + +# 写代码 + +- 只改完成任务所需的代码;不 drive-by 重构、不编辑无关文件、不扩 scope;不写无调用链的示范代码、不夹带 demo 文件。 +- 写之前读上下文;命名、类型、抽象、import、注释密度与现有代码一致;优先复用已有函数/组件。 +- diff 中每行都应对任务负责:不写冗长注释/显而易见 docstring/多余变量/过度 try-catch;优先统一路径而非堆分支。 +- 不删与任务无关的注释或代码(避免难审 diff)。 +- UI:间距、字体、颜色、布局与现有设计一致。 diff --git a/.cursor/rules/git-commit.mdc b/.cursor/rules/git-commit.mdc new file mode 100644 index 0000000..821d45c --- /dev/null +++ b/.cursor/rules/git-commit.mdc @@ -0,0 +1,19 @@ +--- +description: 仅在用户明确要求时 git commit;安全协议 +alwaysApply: true +--- + +# Git commit + +- 仅当用户在当前对话中明确要求创建 commit 时才提交;若含糊先问。用户未要求 commit 则不要提交。 +- 禁止:`git config` 任何修改。 +- 禁止:未获用户明确要求的破坏性命令(如 `push --force`、`reset --hard` 等)。 +- 禁止:跳过 hooks(`--no-verify`、`--no-gpg-sign` 等)除非用户明确要求。 +- 禁止:`git push --force` 到 `main`/`master`;若用户要求则先警告。 +- 避免 `git commit --amend`。仅当同时满足:用户明确要求 amend,或 commit 成功但 hook 自动改了文件需纳入;且 HEAD 为你本会话所建;且未 push(`git status` 显示 ahead 且未 push)——才 amend。commit 失败或被 hook 拒绝:禁止 amend,修问题后新建 commit。已 push:禁止 amend(除非用户明确要求并知悉需 force)。 +- 禁止:`git` 带 `-i` 的交互命令。 +- 无改动则不要空 commit。 +- 不要 stage/提交明显密钥文件(`.env`、`credentials.json` 等);用户若坚持提交须警告。 +- 创建 commit 时并行执行:`git status`、`git diff`(staged+unstaged)、`git log`(看消息风格);该阶段除 git 外不要用其它命令读仓库/扫代码;分析后写 message;再顺序:`git add` 相关文件 → `git commit` → `git status` 校验。 +- commit message 用 HEREDOC 传入(示例):`git commit -m "$(cat <<'EOF'\n...\nEOF\n)"`。 +- 禁止:用户未明确要求则不要 `git push`。 diff --git a/.cursor/rules/github-pr.mdc b/.cursor/rules/github-pr.mdc new file mode 100644 index 0000000..7d81a7c --- /dev/null +++ b/.cursor/rules/github-pr.mdc @@ -0,0 +1,14 @@ +--- +description: GitHub 任务用 gh;创建 PR 流程 +alwaysApply: true +--- + +# GitHub / PR + +- 所有 GitHub 相关操作(issues、PR、checks、releases、从 URL 取信息)用 `gh`,通过 Shell 执行。 +- 创建 PR 前并行:`git status`、`git diff`、检查当前分支是否跟踪远端及是否与远端同步、`git log` + `git diff ...HEAD`(覆盖从 base 分叉起的全部提交,非只看最新一条)。 +- 分析将纳入 PR 的全部改动与全部相关 commit,再写 PR 摘要。 +- 顺序:必要时建新分支 → 需要时 `git push -u origin HEAD` → `gh pr create`;`--body` 用 HEREDOC。 +- PR body 结构:`## Summary`(1~3 条)、`## Test plan`(可勾选清单)。 +- 完成后把 PR URL 返回给用户。 +- 禁止:`git config` 修改;禁止用户未要求时 `push`。 diff --git a/.cursor/skills/electron-vue2-architecture/SKILL.md b/.cursor/skills/electron-vue2-architecture/SKILL.md new file mode 100644 index 0000000..ebf4aa5 --- /dev/null +++ b/.cursor/skills/electron-vue2-architecture/SKILL.md @@ -0,0 +1,633 @@ +--- +name: electron-vue2-architecture +description: >- + 生成 Electron + Vue2 + Element UI + Vuex 桌面端标准前端工程;HTTP/Sign/Token/分页/Mock + 与 FastAdmin AppApi 对齐。用于新建项目、域模块、页面、mock 与 API 联调。 +--- + +# Electron + Vue2(FastAdmin AppApi) + +## FastAdmin 联动 + +| 后端 | 前端 | +|------|------| +| `application/api/controller/*.php` 继承 `AppApi` | `src/modules/<域>/api.ts` 同 path | +| `.env` `[api] app_id` `app_secret` `sign_enable` | `VUE_APP_API_*`(secret 仅 `.local`) | +| `HTTP Token` | `Authorization: Bearer {token}` | +| `successPaginate()` | `PaginateData` | +| `application/extra/api.php` `skip_sign` | 无需签名(支付回调等) | + +路径:`/api/{controller}/{action}`。成功 `code=1`。分页 `page` `limit` → `{ list, total, page, limit }`。 + +```typescript +// types/api.ts +export interface ApiResult { + code: number + msg: string + time: number + data: T + request_id?: string +} + +export interface PaginateData { + list: T[] + total: number + page: number + limit: number +} + +export class ApiError extends Error { + constructor(public code: number, message: string, public requestId?: string) { + super(message) + this.name = 'ApiError' + } +} +``` + +| code | 含义 | +|------|------| +| 1 | 成功 | +| 0 | 失败 | +| 401 | 未登录 | +| 403 | 无权限 | +| 4001 | 签名/缺参 | +| 4002 | 时间戳过期 | +| 4003 | nonce 重复 | +| 4004 | app_id 无效 | + +## 签名(ApiSign) + +Query + Body 全参数 → 去 `sign` → 写 `app_id` `timestamp` `nonce` → ksort → `k=v&` → `strtolower(hmac_sha256(plain, secret))`。 + +Header:`X-App-Id` `X-Timestamp` `X-Nonce` `X-Sign`。空字符串参与拼接;`object/array` 用 `JSON.stringify`。 + +## 原则 + +- views → composable → `service.ts` → `mock.ts` | `api.ts` → `helpers` → `core`。 +- store actions → `service.ts`;mutation 不发 HTTP。 +- 契约不明则停;单迭代单域。 +- `VUE_APP_USE_MOCK`:`true` | `hybrid` | `false`;production 必 `false`。 +- secret 不进仓库。 +- 渲染进程 Node API 走 preload + `contextBridge`。 +- Vuex `namespaced` 模块化。 + +## 技术栈 + +Electron · Vue CLI 5 · Vue2.7 · TS · Vuex 4 · vue-router@3 · Element UI · scss · axios · js-sha256 · electron-store · electron-builder + +## 目录 + +```text +electron/{main,preload,security}.ts electron/ipc/{index,store}.ts electron/tsconfig.json +src/types/{api,global}.ts config.ts config/mock-routes.ts +src/services/{sign,request}.ts services/http/{core,helpers}.ts +src/modules/<域>/{types,api,service,mock,index}.ts +src/modules/navigation/{core,routes,<域>}.ts +src/store/{index,types}.ts store/modules/user.ts +src/router/{index,routes,guards}.ts router/modules/<域>.ts +src/composables/usePageState.ts components/{PageShell,PageState}.vue +src/layouts/DefaultLayout.vue views/<域>/.vue +src/main.ts App.vue styles/{variables,element-overrides}.scss +mock/v1/<资源>/.json +.env.development .env.production .env.example +vue.config.js electron-builder.yml package.json +``` + +命名:`apiXxx` · `fetchXxx` · `mockXxx` · `goXxx` · `useXxx` + +## 环境变量 + +| 变量 | 默认 | 说明 | +|------|------|------| +| `VUE_APP_API_BASE` | — | 根 URL,无尾斜杠 | +| `VUE_APP_USE_MOCK` | `true` | `true`/`hybrid`/`false` | +| `VUE_APP_API_SUCCESS_CODE` | `1` | | +| `VUE_APP_API_SIGN_ENABLED` | `true` | 对齐 `[api] sign_enable` | +| `VUE_APP_API_APP_ID` | — | 对齐 `[api] app_id` | +| `VUE_APP_API_SIGN_SECRET` | — | 仅 `.env.development.local` | +| `VUE_APP_DEV_SERVER_URL` | — | Electron dev 加载地址 | + +`.env.example` 占位;gitignore `*.local`。 + +## config.ts + +```typescript +export const API_BASE = String(process.env.VUE_APP_API_BASE || '').trim().replace(/\/$/, '') +export const USE_MOCK = process.env.VUE_APP_USE_MOCK === 'true' +export const MOCK_MODE = String(process.env.VUE_APP_USE_MOCK || 'false') +export const API_SUCCESS_CODE = Number(process.env.VUE_APP_API_SUCCESS_CODE ?? 1) +export const API_SIGN_ENABLED = process.env.VUE_APP_API_SIGN_ENABLED === 'true' +export const API_APP_ID = String(process.env.VUE_APP_API_APP_ID || '').trim() +export const API_SIGN_SECRET = String(process.env.VUE_APP_API_SIGN_SECRET || '').trim() +``` + +## config/mock-routes.ts + +```typescript +import { MOCK_MODE } from '../config' + +type RouteKey = `${Uppercase} ${string}` + +const REAL_ROUTES = new Set([ + // 'POST /api/user/login', +]) + +export function shouldUseRealApi(method: string, path: string): boolean { + if (MOCK_MODE === 'false') return true + if (MOCK_MODE !== 'hybrid') return false + return REAL_ROUTES.has(`${method.toUpperCase()} ${path}` as RouteKey) +} +``` + +## services/sign.ts + +```typescript +import sha256 from 'js-sha256' +import { API_APP_ID, API_SIGN_ENABLED, API_SIGN_SECRET } from '../config' + +function encode(v: unknown): string { + if (Array.isArray(v) || (v !== null && typeof v === 'object')) return JSON.stringify(v) + return String(v ?? '') +} + +function mergeParams(url: string, data?: unknown): Record { + const out: Record = {} + const i = url.indexOf('?') + if (i >= 0) new URLSearchParams(url.slice(i + 1)).forEach((v, k) => { out[k] = v }) + if (data && typeof data === 'object' && !Array.isArray(data)) { + for (const [k, v] of Object.entries(data as Record)) { + if (v === undefined) continue + out[k] = encode(v) + } + } + return out +} + +export function buildSignHeaders(_method: string, url: string, data?: unknown): Record { + if (!API_SIGN_ENABLED || !API_SIGN_SECRET || !API_APP_ID) return {} + const timestamp = String(Math.floor(Date.now() / 1000)) + const nonce = `${Date.now()}_${Math.random().toString(36).slice(2, 12)}` + const params = mergeParams(url, data) + delete params.sign + params.app_id = API_APP_ID + params.timestamp = timestamp + params.nonce = nonce + const plain = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join('&') + return { + 'X-App-Id': API_APP_ID, + 'X-Timestamp': timestamp, + 'X-Nonce': nonce, + 'X-Sign': sha256.hmac(API_SIGN_SECRET, plain).toLowerCase(), + } +} +``` + +## services/http/core.ts + +```typescript +import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import { API_BASE, API_SUCCESS_CODE } from '../../config' +import { buildSignHeaders } from '../sign' +import type { ApiResult } from '../../types/api' + +export type HttpOptions = AxiosRequestConfig & { skipAuth?: boolean } + +let onUnauthorized: (() => void) | null = null +export function setUnauthorizedHandler(fn: (() => void) | null) { onUnauthorized = fn } +export function isApiSuccess(code: number): boolean { return code === API_SUCCESS_CODE } + +function resolveUrl(path: string): string { + if (/^https?:\/\//i.test(path)) return path + const base = API_BASE.replace(/\/$/, '') + return `${base}${path.startsWith('/') ? path : `/${path}`}` +} + +const http = axios.create({ baseURL: API_BASE, timeout: 15000 }) + +http.interceptors.request.use((cfg: InternalAxiosRequestConfig & { skipAuth?: boolean }) => { + cfg.url = resolveUrl(cfg.url || '') + const method = (cfg.method || 'GET').toUpperCase() + Object.assign(cfg.headers, buildSignHeaders(method, cfg.url || '', method === 'GET' ? cfg.params : cfg.data)) + if (!cfg.skipAuth) { + const token = window.electronAPI?.getToken?.() ?? window.localStorage.getItem('token') ?? '' + if (token) cfg.headers.Authorization = `Bearer ${token}` + } + return cfg +}) + +http.interceptors.response.use( + (res: AxiosResponse) => { + const body = res.data + if (!body || typeof body.code !== 'number') return Promise.reject(new Error('invalid response')) + if (body.code === 401 && onUnauthorized) onUnauthorized() + return res + }, + (err) => Promise.reject(err), +) + +export async function httpRequest(options: HttpOptions): Promise> { + return (await http.request>(options)).data +} +``` + +## services/http/helpers.ts + +```typescript +import { ApiError, ApiResult } from '../../types/api' +import { API_SUCCESS_CODE } from '../../config' +import { httpRequest } from './core' + +export function unwrapApi(res: ApiResult): T { + if (res.code !== API_SUCCESS_CODE) throw new ApiError(res.code, res.msg || 'request failed', res.request_id) + return res.data +} + +export function apiGet(path: string, params?: Record, skipAuth = false) { + return httpRequest({ url: path, method: 'GET', params, skipAuth }) +} + +export function apiPost(path: string, data?: unknown, skipAuth = false) { + return httpRequest({ url: path, method: 'POST', data, skipAuth }) +} +``` + +## services/request.ts + +```typescript +export type { ApiResult, PaginateData, ApiError } from '../types/api' +export { setUnauthorizedHandler, isApiSuccess } from './http/core' +export { unwrapApi, apiGet, apiPost } from './http/helpers' +export type { HttpOptions } from './http/core' +``` + +## 域模块 + +每域必备:`types.ts` `api.ts` `service.ts` `mock.ts` `index.ts`。首域 `auth`(login/logout)。 + +```typescript +// api.ts +import { apiGet, apiPost } from '@/services/request' + +export function apiLogin(input: LoginInput) { + return apiPost('/api/user/login', input, true) +} + +// service.ts +import { USE_MOCK } from '@/config' +import { shouldUseRealApi } from '@/config/mock-routes' +import { unwrapApi, apiGet } from '@/services/request' +import { apiLogin } from './api' +import { mockXxx } from './mock' + +export async function fetchXxx(input: XxxQuery) { + const path = '/api//' + const method = 'GET' + if (USE_MOCK && !shouldUseRealApi(method, path)) return mockXxx(input) + return unwrapApi(await apiGet(path, input)) +} + +// mock.ts +// index.ts — export * from './types'; export * from './service' +``` + +客户端校验失败:返回 `{ code: 40001, msg, data: null }`,不发 HTTP。 + +## store/types.ts + +```typescript +import type { UserState } from './modules/user' +export interface RootState { user: UserState } +``` + +## store/modules/user.ts + +```typescript +import { Module } from 'vuex' +import { fetchLogin, fetchLogout } from '@/modules/auth' +import type { LoginInput } from '@/modules/auth' +import type { RootState } from '../types' + +export interface UserState { + token: string + profile: Record | null + role: 'admin' | 'user' | 'sync' | '' +} + +export const user: Module = { + namespaced: true, + state: () => ({ token: '', profile: null, role: '' }), + getters: { + isLoggedIn: (s) => !!s.token, + hasRole: (s) => (roles: UserState['role'][]) => roles.includes(s.role), + }, + mutations: { + setSession(state, p: { token: string; profile: Record; role: UserState['role'] }) { + state.token = p.token + state.profile = p.profile + state.role = p.role + window.electronAPI?.setToken?.(p.token) + window.localStorage.setItem('token', p.token) + }, + clearSession(state) { + state.token = '' + state.profile = null + state.role = '' + window.electronAPI?.setToken?.('') + window.localStorage.removeItem('token') + }, + hydrate(state) { + state.token = String(window.electronAPI?.getToken?.() ?? window.localStorage.getItem('token') ?? '') + }, + }, + actions: { + async login({ commit }, input: LoginInput) { + const data = await fetchLogin(input) + commit('setSession', { token: data.token, profile: data.profile, role: data.role }) + }, + async logout({ commit }) { + try { await fetchLogout() } catch { /* ignore */ } + commit('clearSession') + }, + hydrate({ commit }) { commit('hydrate') }, + }, +} +``` + +## store/index.ts + +```typescript +import Vue from 'vue' +import Vuex from 'vuex' +import { user } from './modules/user' +import type { RootState } from './types' + +Vue.use(Vuex) +export default new Vuex.Store({ + modules: { user }, + strict: process.env.NODE_ENV !== 'production', +}) +``` + +## router + +`routes.ts`:`ROUTE_NAMES` + `constantRoutes`(login/404/public)。`guards.ts`:未登录 → login;`meta.roles` → `user/hasRole`。`index.ts`:`hash` + `DefaultLayout` children。 + +## composables/usePageState.ts + +```typescript +import { ref, Ref } from 'vue' + +export type PageStatus = 'loading' | 'empty' | 'success' | 'error' + +export function usePageState(loader: () => Promise, isEmpty: (d: T) => boolean) { + const status: Ref = ref('loading') + const data: Ref = ref(null) + const error = ref('') + async function run() { + status.value = 'loading' + error.value = '' + try { + const res = await loader() + data.value = res + status.value = isEmpty(res) ? 'empty' : 'success' + } catch (e) { + error.value = e instanceof Error ? e.message : 'load failed' + status.value = 'error' + } + } + return { status, data, error, run } +} +``` + +## navigation + +```typescript +// core.ts +import router from '@/router' +export function safeNavigate(to: string) { + router.push(to).catch(() => router.push('/')) +} +// routes.ts +// <域>.ts — goXxx() +``` + +## 页面 + +`DefaultLayout` + `PageState` + `usePageState`。四态 `loading|empty|success|error`。>350 行拆 composable。BEM + scoped scss。 + +## main.ts + +```typescript +import Vue from 'vue' +import ElementUI from 'element-ui' +import 'element-ui/lib/theme-chalk/index.css' +import App from './App.vue' +import router from './router' +import store from './store' +import { setUnauthorizedHandler } from '@/services/request' +import '@/styles/element-overrides.scss' + +Vue.use(ElementUI, { size: 'medium' }) +Vue.config.productionTip = false +store.dispatch('user/hydrate') +setUnauthorizedHandler(() => store.dispatch('user/logout')) +new Vue({ router, store, render: (h) => h(App) }).$mount('#app') +``` + +## types/global.d.ts + +```typescript +interface ElectronAPI { + getToken: () => string + setToken: (v: string) => Promise + clearCache: () => Promise + openExternal: (url: string) => Promise +} +declare global { interface Window { electronAPI: ElectronAPI } } +``` + +## electron/main.ts + +```typescript +import { app, BrowserWindow, shell } from 'electron' +import path from 'path' +import { registerIpc } from './ipc' +import { applySecurity } from './security' + +let win: BrowserWindow | null = null + +function createWindow() { + win = new BrowserWindow({ + width: 1280, height: 800, show: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, nodeIntegration: false, sandbox: true, + }, + }) + if (!app.isPackaged && process.env.VUE_APP_DEV_SERVER_URL) { + win.loadURL(process.env.VUE_APP_DEV_SERVER_URL) + } else { + win.loadFile(path.join(__dirname, '../dist/index.html')) + } + win.once('ready-to-show', () => win?.show()) + win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' } }) + applySecurity(win) +} + +app.whenReady().then(() => { + registerIpc() + createWindow() + app.on('activate', () => { if (!BrowserWindow.getAllWindows().length) createWindow() }) +}) +app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) +``` + +## electron/preload.ts + +```typescript +import { contextBridge, ipcRenderer } from 'electron' + +contextBridge.exposeInMainWorld('electronAPI', { + getToken: (): string => ipcRenderer.invoke('token:get'), + setToken: (v: string): Promise => ipcRenderer.invoke('token:set', v), + clearCache: (): Promise => ipcRenderer.invoke('cache:clear'), + openExternal: (url: string): Promise => ipcRenderer.invoke('shell:openExternal', url), +}) +``` + +## electron/security.ts + +```typescript +import { BrowserWindow, session } from 'electron' + +export function applySecurity(win: BrowserWindow) { + win.webContents.on('will-navigate', (e) => e.preventDefault()) + session.defaultSession.webRequest.onHeadersReceived((details, cb) => { + cb({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': ["default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http:; connect-src 'self' " + (process.env.VUE_APP_API_BASE || '') + ";"], + }, + }) + }) +} +``` + +## electron/ipc + +```typescript +// index.ts +import { registerStoreIpc } from './store' +export function registerIpc() { registerStoreIpc() } + +// store.ts +import { ipcMain } from 'electron' +import Store from 'electron-store' +const store = new Store() +export function registerStoreIpc() { + ipcMain.handle('token:get', () => store.get('token', '')) + ipcMain.handle('token:set', (_e, v: string) => store.set('token', v)) + ipcMain.handle('cache:clear', async () => undefined) + ipcMain.handle('shell:openExternal', (_e, url: string) => { require('electron').shell.openExternal(url) }) +} +``` + +## vue.config.js + +```javascript +const { defineConfig } = require('@vue/cli-service') +const path = require('path') + +module.exports = defineConfig({ + publicPath: './', + outputDir: 'dist', + productionSourceMap: false, + configureWebpack: { resolve: { alias: { '@': path.resolve(__dirname, 'src') } } }, + css: { loaderOptions: { sass: { additionalData: '@import "@/styles/variables.scss";' } } }, + devServer: { + port: 9080, + proxy: { '/api': { target: process.env.VUE_APP_DEV_PROXY_TARGET || 'http://127.0.0.1:8080', changeOrigin: true } }, + }, +}) +``` + +## electron-builder.yml + +```yaml +appId: com.xenon.client +productName: XENON-Client +directories: + output: dist_electron + buildResources: build +files: + - dist/** + - electron/** + - package.json + - "!**/*.map" +win: + target: nsis + artifactName: ${productName}-${version}-${arch}.${ext} +nsis: + oneClick: false + allowToChangeInstallationDirectory: true +``` + +## package.json + +```json +{ + "scripts": { + "dev:web": "vue-cli-service serve", + "dev:electron": "npm run build:electron && cross-env VUE_APP_DEV_SERVER_URL=http://127.0.0.1:9080 electron .", + "build:web": "vue-cli-service build", + "build:electron": "tsc -p electron/tsconfig.json", + "dist": "npm run build:web && electron-builder", + "typecheck": "vue-tsc --noEmit" + } +} +``` + +## 初始化顺序 + +```text +1. npx @vue/cli create (Vue2 + TS + Router + Vuex) +2. npm i axios element-ui js-sha256 electron-store +3. npm i -D electron electron-builder @types/electron cross-env sass +4. electron/* + src/types/* + config* + services/* +5. store/* + router/* + modules/auth/* + modules/navigation/* +6. composables/* + components/PageState.vue + layouts/DefaultLayout.vue +7. views/auth/Login.vue + main.ts + .env* +8. vue.config.js + electron-builder.yml +9. npm run typecheck && npm run dev:web && npm run dev:electron +10. hybrid:REAL_ROUTES 增量;sign/Token 通过后扩域 +``` + +## 新建域 Checklist + +```text +[ ] modules/<域>/{types,mock,api,service,index}.ts +[ ] mock/v1/*.json +[ ] router/modules/<域>.ts +[ ] navigation/<域>.ts + views/<域>/*.vue +[ ] 四态 + typecheck +[ ] meta.roles(若需) +[ ] hybrid 登记 path +``` + +## 禁止 + +- 页面/Store import mock 或 api +- api.ts mock 分支 +- 域内重复 apiGet/apiPost +- 自创签名算法 +- 渲染进程 `require('fs'|'child_process'|'electron')` +- `nodeIntegration: true` 或 `contextIsolation: false` +- `header.Token`(用 `Authorization: Bearer`) +- production mock 或 secret 入库 +- 页面 axios 裸调 +- mutation 内发 HTTP + +## 例外 + +- `window.electronAPI` IPC 白名单 +- `electron-builder` 目录 `build/` diff --git a/.cursor/skills/fullstack-delivery-workflow/SKILL.md b/.cursor/skills/fullstack-delivery-workflow/SKILL.md new file mode 100644 index 0000000..b733702 --- /dev/null +++ b/.cursor/skills/fullstack-delivery-workflow/SKILL.md @@ -0,0 +1,249 @@ +--- +name: fullstack-delivery-workflow +description: >- + XENON 双轨交付工作流:Electron 客户端页+Mock(轨道 A)、FastAdmin 业务模块+联调(轨道 B)。 + 含 progress 快照、模块签收表、四则固定提示词。配合 electron-vue2-architecture 与 + docs/api 契约使用。 +--- + +# XENON 全栈开发工作流 + +## 与架构 Skill 分工 + +| Skill | 职责 | +|-------|------| +| electron-vue2-architecture | Electron 目录、HTTP、Mock、Sign、Vuex、域模块代码 | +| 本文 | 分批、自检、签收、progress 维护 | + +执行顺序:读本文定轨道与批次 → 读 electron-vue2-architecture 写代码 → 批末门禁 → 只更新 progress 快照。 + +## 项目边界 + +| 项 | 路径 / 约定 | +|----|-------------| +| 后端 | `backend-xenon/`(FastAdmin · ThinkPHP) | +| 客户端 | Electron + Vue2 + Element UI(待建或 `client/` 目录) | +| API 契约 | `docs/api/客户端接口文档.md`、`docs/api/admin端接口文档.md`、`docs/api/数据库文档.md` | +| 总方案 | `docs/开发方案.md` | +| 本地联调 | `backend-xenon/scripts/start.bat` → Admin `8080`、MySQL `13306` | +| 同步 / 离线 | 非本工作流范围 | + +接口 url「待复核」处:按业务语义找接口,**不擅自改**文档(见 `开发方案 §3.4`)。 + +## 双轨 + +| 轨道 | 单位 | Mock | 何时用 | +|------|------|------|--------| +| A | 1~3 页,同业务域 | `VUE_APP_USE_MOCK=true` | Electron UI + Mock、页契约 | +| B | 1 业务模块(该模块全部 path) | `hybrid` → `false` | 后端 Controller/Service/SQL + 客户端真服 + Admin | + +推荐:轨道 A 阶段签收后再大规模 B;某模块 Mock 已稳可单模块直接 B。 + +## 模块 ID(P0 优先) + +| ID | 名称 | 契约来源 | +|----|------|----------| +| M01 | 登录 / 鉴权 | 客户端 §1 + admin §1 | +| M02 | 权限中间件 | `开发方案 §6` | +| M03 | 行业 / 客户 / 工艺 / 产品类型 | admin 文档对应章节 | +| M04 | 产品 + 媒体 | admin + 客户端产品相关 | +| M05 | admin 用户与角色 | admin RBAC | +| M06 | 客户端用户 | admin 客户端用户 + 客户端登录 | +| M07 | 标题简介 | admin + 客户端 | +| M08 | 客户端列表 / 详情 | 客户端文档 | +| M09 | 浏览历史 / 缓存 | 客户端文档 | +| M10 | 导入 / 导出 | admin + 客户端 | + +## Agent 执行协议 + +1. 读 `docs/progress.md`「进度快照」。 +2. 「下一批」→ **原样复制**对应轨道「下一批提示词」;「自检」→ 复制「批次自检提示词」。 +3. 禁止改写四则提示词正文(用户明确要求改版除外)。 +4. 回复首段:`本批:…` 或 `自检:…`。 +5. 先 todos → 执行 → 批末门禁 → **只改 progress 快照**(rollout 勾选除外)。 +6. 冲突时:`docs/module-rollout.md` 优先,与快照「建议下一批」交叉核对。 + +## 轨道 A — 单批(7 步) + +1. 从 `docs/api/客户端接口文档.md` 确认 path、method(禁止自造)。 +2. `modules/<域>/types.ts` +3. `modules/<域>/api.ts` + `service.ts` + `mock.ts` + `index.ts` +4. `mock/v1/<资源>/.json` +5. `views/<域>/.vue` + `composables/useXxx.ts`(仅 import service) +6. `router/modules/<域>.ts` 挂 `DefaultLayout` children +7. 批末:`npm run typecheck`;本批 path 已在 mock 覆盖 + +### 单页 DoD + +- UI 可导航,主流程可点。 +- 域五件套:types、api、service、mock、view。 +- mock 响应 `{code,msg,data}` 与 FastAdmin 约定一致(成功 `code=1`)。 +- 页面无 import api/mock、无 axios 裸调。 + +### 阶段 A 签收(切入 B 前置) + +| # | 条件 | +|---|------| +| 1 | 计划内客户端页契约全覆盖 | +| 2 | 声明 path ⊆ mock/v1 或 service 覆盖 | +| 3 | `npm run typecheck` PASS | +| 4 | `npm run dev:web` 或 `dev:electron` 可启动 | +| 5 | 主流程手测 5~7 条(登录 → 列表 → 详情) | + +## 轨道 B — M-Batch(5 步) + +| 步 | 内容 | DoD | +|----|------|-----| +| ① 契约 | mock + types + `docs/api` 字段对照 | mock JSON ≡ types ≡ 将实现响应 | +| ② 后端 | `application/api`(客户端)+ `application/admin`(后台)薄 Controller + Service + 迁移/SQL | 本模块 path curl **0 fail**(Docker 8080) | +| ③ Admin | FastAdmin 菜单/权限 + CRUD 页;**复用** Service | 无需求标 N/A,rollout Admin 列写 — | +| ④ hybrid | `config/mock-routes.ts` REAL_ROUTES 登记 | 抽测 1~3 条主路径 | +| ⑤ 签收 | rollout 勾选 + 更新快照 | 本行 客户端/后端/manifest/抽测已勾 | + +### 模块 DoD + +- 五步完成;本模块 curl 0 fail;mock 与真服形状一致(差异记入 rollout 备注)。 + +## Mock 三档 + +| VUE_APP_USE_MOCK | 行为 | +|------------------|------| +| true | 全 mock | +| hybrid | REAL_ROUTES 命中 → 真 API;未命中 → mock | +| false | 全真服 | + +Mock / hybrid-mock **不发送**签名 Header。 + +## 批末命令(项目现状) + +| 轨道 | 每批 | 里程碑 | +|------|------|--------| +| A | `npm run typecheck` | + `dev:electron` 冒烟 | +| B | 本模块 curl 清单 0 fail | + hybrid 抽测 + `typecheck` | + +后端抽测示例:`curl -X POST http://127.0.0.1:8080/api/...`(带 Bearer / Sign 按 `.env`)。 + +## 模块签收表(rollout) + +| 列 | 含义 | +|----|------| +| 模块 ID | M01…M10 | +| 名称 | 业务模块 | +| API 数 | 本模块 path 数 | +| 客户端 | Electron 页/service 已实现 | +| 后端 | api/admin Controller 已实现 | +| Admin | FastAdmin 后台(— 表示 N/A) | +| manifest | REAL_ROUTES 已登记 | +| 抽测 | hybrid 主路径通过 | +| 签收日期 | | + +首个未签收:客户端 / 后端 / manifest / 抽测 任一未勾。 + +## 自检 vs 下一批 + +| | 自检 | 下一批 | +|---|------|--------| +| 目的 | 验 DoD | 实现新一批 | +| 首段 | `自检:…` | `本批:…` | +| 写代码 | 否(补漏除外) | 是 | +| 改 progress | 仅快照 | 快照 + rollout | + +## 禁止 + +- 改 progress 内四则提示词(用户明确要求除外) +- 自检重复实现整批 +- 无模块 ID / 无域边界空回归 +- 未登记 REAL_ROUTES 即宣称模块完成 +- api Controller 与 admin Controller 各写一套重复业务逻辑 +- 未读 `docs/api` 猜字段 +- 跨域同批大改 +- 页内 mock 数据或散落 HTTP +- 擅自修改 API 文档「待复核」url + +--- + +## progress.md 模板(`docs/progress.md`) + +```markdown +# progress.md + +> Agent 只改「进度快照」;勿改下方四则提示词。 + +## 进度快照 + +| 项 | 填写 | +|----|------| +| 更新日期 | | +| 当前轨道 | A / B | +| 当前模块/域 | | +| 建议下一批 | | +| 本批已完成 | | + +## 轨道A — 批次自检提示词 +(粘贴 SKILL 对应代码块全文) + +## 轨道A — 下一批提示词 +(粘贴 SKILL 对应代码块全文) + +## 轨道B — 批次自检提示词 +(粘贴 SKILL 对应代码块全文) + +## 轨道B — 下一批提示词 +(粘贴 SKILL 对应代码块全文) +``` + +## module-rollout.md 模板(`docs/module-rollout.md`) + +```markdown +# module-rollout.md + +| ID | 名称 | API数 | 客户端 | 后端 | Admin | manifest | 抽测 | 签收日期 | 备注 | +|----|------|-------|--------|------|-------|----------|------|----------|------| +| M01 | 登录鉴权 | | | | | | | | | +``` + +--- + +## 四则提示词(固定正文,写入 progress.md) + +### 轨道A — 下一批提示词 + +```text +执行轨道A下一批(Electron 页+Mock)。先建 todos。须配合 electron-vue2-architecture。自判:读 progress「建议下一批」;无则从 docs/api/客户端接口文档.md 取同域未实现页,每批 1~3 页。首段「本批:轨道A <域> <页名>」。① docs/api 定 path ② modules/<域>/{types,api,service,mock,index} ③ mock/v1/*.json ④ views/<域>/*.vue + composable ⑤ router/modules/<域>.ts。批末 npm run typecheck。禁跨域同批、禁页内 axios/mock、禁自造 API。只改 progress 快照。 +``` + +### 轨道A — 批次自检提示词 + +```text +轨道A自检本批。先建 todos。自判:本对话刚完成页优先,否则读快照「当前模块/域」。首段「自检:轨道A <域> <页名>」。核对:域五件套齐全;本批 path mock 已覆盖;页面仅 import service;主路径 mock 可点。批末 npm run typecheck。只改 progress 快照。 +``` + +### 轨道B — 下一批提示词 + +```text +执行轨道B M-Batch。先建 todos。须配合 electron-vue2-architecture + backend-xenon。自判:读 progress + module-rollout + docs/api;按 M01→M10 取首个未签收(客户端/后端/manifest/抽测未全勾)。首段「本批:Mxx <名>」。① 契约:mock+types 对照 docs/api ② backend-xenon Controller+Service+SQL,本模块 curl 0 fail ③ FastAdmin Admin(N/A 写 —)④ config/mock-routes REAL_ROUTES + hybrid 抽测 1~3 条 ⑤ rollout 勾选+快照。禁无模块 ID、禁未 manifest 即完成、禁重复 Service 逻辑。只改 progress 快照。 +``` + +### 轨道B — 批次自检提示词 + +```text +轨道B M-Batch 自检。先建 todos。自判:本对话模块优先,否则读快照+rollout 对应行+docs/api。首段「自检:Mxx <名>」。核对:mock/types 与 Service 响应一致;本模块 curl 0 fail;Admin 复用 Service 或 N/A;REAL_ROUTES 已登记;rollout 本行已勾。只改 progress 快照。 +``` + +--- + +## 初始化 Checklist + +```text +[ ] docs/progress.md(快照 + 四则提示词) +[ ] docs/module-rollout.md(M01~M10 空表) +[ ] 客户端工程按 electron-vue2-architecture 初始化 +[ ] backend-xenon/scripts/start.bat 可启动 +[ ] .env:VUE_APP_API_BASE=http://127.0.0.1:8080 +[ ] 首批:M01 或轨道 A 登录页,更新 progress 快照 +``` + +## 非琐碎开发勾选 + +- 轨道 A:`electron-vue2-architecture` + `fullstack-delivery-workflow` +- 轨道 B:同上 diff --git a/.cursor/skills/uniapp-cli-architecture/SKILL.md b/.cursor/skills/uniapp-cli-architecture/SKILL.md new file mode 100644 index 0000000..988eb85 --- /dev/null +++ b/.cursor/skills/uniapp-cli-architecture/SKILL.md @@ -0,0 +1,435 @@ +--- +name: uniapp-cli-architecture +description: >- + 生成 uni-app Vue3 Vite TS 标准前端工程;HTTP/Sign/Token/分页/Mock 与 FastAdmin AppApi 对齐。 + 用于新建项目、域模块、页面、mock 与 API 联调。 +--- + +# uni-app CLI(FastAdmin AppApi) + +## FastAdmin 联动 + +| 后端 | 前端 | +|------|------| +| `application/api/controller/*.php` 继承 `AppApi` | `modules/<域>/api.ts` 同 path | +| `.env` `[api] app_id` `app_secret` `sign_enable` | `VITE_API_*`(secret 仅 `.local`) | +| `HTTP Token` | `header.Token` | +| `successPaginate()` | `PaginateData` | +| `application/extra/api.php` `skip_sign` | 无需签名(支付回调等) | + +路径:`/api/{controller}/{action}`。成功 `code=1`。分页 `page` `limit` → `{ list, total, page, limit }`。 + +```typescript +// types/api.ts +export interface ApiResult { + code: number + msg: string + time: number + data: T + request_id?: string +} + +export interface PaginateData { + list: T[] + total: number + page: number + limit: number +} +``` + +| code | 含义 | +|------|------| +| 1 | 成功 | +| 0 | 失败 | +| 401 | 未登录 | +| 403 | 无权限 | +| 4001 | 签名/缺参 | +| 4002 | 时间戳过期 | +| 4003 | nonce 重复 | +| 4004 | app_id 无效 | + +## 签名(ApiSign) + +Query + Body 全参数 → 去 `sign` → 写 `app_id` `timestamp` `nonce` → ksort → `k=v&` → `strtolower(hmac_sha256(plain, secret))`。 + +Header:`X-App-Id` `X-Timestamp` `X-Nonce` `X-Sign`。空字符串参与拼接;`object/array` 用 `JSON.stringify`。 + +## 原则 + +- 页面 → `service.ts` → `mock.ts` | `api.ts` → `request()`。 +- 契约不明则停;单迭代单域。 +- `VITE_USE_MOCK`:`true` | `hybrid` | `false`;production 必 `false`。 +- secret 不进仓库;小程序禁内置 secret。 + +## 技术栈 + +uni-app CLI · Vite · Vue3 · TS · Pinia · scss · uni-ui · js-sha256 + +## 目录 + +```text +src/ +├── types/api.ts +├── config.ts +├── config/mock-routes.ts +├── services/http/core.ts +├── services/request.ts +├── services/sign.ts +├── modules/<域>/{types,api,service,mock,index}.ts +├── modules/navigation/{core,routes,<域>}.ts +├── stores/ +├── composables/ +├── components/{PageShell,PageState}.vue +├── pages/ +├── package-<域>/ +├── mock/v1/<资源>/.json +├── pages.json manifest.json uni.scss main.ts App.vue +.env.development .env.production .env.example +``` + +命名:`apiXxx` · `fetchXxx` · `mockXxx` · `goXxx` + +## 环境变量 + +| 变量 | 默认 | 说明 | +|------|------|------| +| `VITE_API_BASE` | — | 根 URL,无尾斜杠;H5 dev 可空 | +| `VITE_USE_MOCK` | `true` | `true`/`hybrid`/`false` | +| `VITE_API_SUCCESS_CODE` | `1` | | +| `VITE_API_SIGN_ENABLED` | `true` | 对齐 `[api] sign_enable` | +| `VITE_API_APP_ID` | — | 对齐 `[api] app_id` | +| `VITE_API_SIGN_SECRET` | — | 仅 `.env.development.local` | + +`.env.example` 占位;gitignore `*.local`。 + +## config.ts + +```typescript +export const API_BASE = String(import.meta.env.VITE_API_BASE || '').trim().replace(/\/$/, '') +export const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true' +export const MOCK_MODE = String(import.meta.env.VITE_USE_MOCK || 'false') +export const API_SUCCESS_CODE = Number(import.meta.env.VITE_API_SUCCESS_CODE ?? 1) +export const API_SIGN_ENABLED = import.meta.env.VITE_API_SIGN_ENABLED === 'true' +export const API_APP_ID = String(import.meta.env.VITE_API_APP_ID || '').trim() +export const API_SIGN_SECRET = String(import.meta.env.VITE_API_SIGN_SECRET || '').trim() +``` + +## config/mock-routes.ts + +```typescript +import { MOCK_MODE } from '../config' + +type RouteKey = `${Uppercase} ${string}` + +const REAL_ROUTES = new Set([ + // 'POST /api/user/login', +]) + +export function shouldUseRealApi(method: string, path: string): boolean { + if (MOCK_MODE === 'false') return true + if (MOCK_MODE !== 'hybrid') return false + return REAL_ROUTES.has(`${method.toUpperCase()} ${path}` as RouteKey) +} +``` + +## services/sign.ts + +```typescript +import sha256 from 'js-sha256' +import { API_APP_ID, API_SIGN_ENABLED, API_SIGN_SECRET } from '../config' + +function encode(v: unknown): string { + if (Array.isArray(v) || (v !== null && typeof v === 'object')) { + return JSON.stringify(v) + } + return String(v ?? '') +} + +function mergeParams(url: string, data?: unknown): Record { + const out: Record = {} + const i = url.indexOf('?') + if (i >= 0) { + new URLSearchParams(url.slice(i + 1)).forEach((v, k) => { + out[k] = v + }) + } + if (data && typeof data === 'object' && !Array.isArray(data)) { + for (const [k, v] of Object.entries(data as Record)) { + if (v === undefined) continue + out[k] = encode(v) + } + } + return out +} + +export function buildSignHeaders(_method: string, url: string, data?: unknown): Record { + if (!API_SIGN_ENABLED || !API_SIGN_SECRET || !API_APP_ID) return {} + const timestamp = String(Math.floor(Date.now() / 1000)) + const nonce = `${Date.now()}_${Math.random().toString(36).slice(2, 12)}` + const params = mergeParams(url, data) + delete params.sign + params.app_id = API_APP_ID + params.timestamp = timestamp + params.nonce = nonce + const plain = Object.keys(params) + .sort() + .map((k) => `${k}=${params[k]}`) + .join('&') + return { + 'X-App-Id': API_APP_ID, + 'X-Timestamp': timestamp, + 'X-Nonce': nonce, + 'X-Sign': sha256.hmac(API_SIGN_SECRET, plain).toLowerCase(), + } +} +``` + +## services/http/core.ts + +```typescript +import { API_BASE, API_SUCCESS_CODE } from '../../config' +import { buildSignHeaders } from '../sign' +import type { ApiResult } from '../../types/api' + +export type HttpOptions = Omit & { + url: string + skipAuth?: boolean +} + +function resolveUrl(path: string): string { + if (/^https?:\/\//i.test(path)) return path + const base = API_BASE.replace(/\/$/, '') + return `${base}${path.startsWith('/') ? path : `/${path}`}` +} + +export function compactQuery(data?: Record) { + if (!data) return undefined + const out: Record = {} + for (const [k, v] of Object.entries(data)) { + if (v !== undefined && v !== null && v !== '') out[k] = v + } + return Object.keys(out).length ? out : undefined +} + +const TOKEN_KEY = 'token' +let onUnauthorized: (() => void) | null = null + +export function setUnauthorizedHandler(fn: (() => void) | null) { + onUnauthorized = fn +} + +export function isApiSuccess(code: number): boolean { + return code === API_SUCCESS_CODE +} + +export async function httpRequest(options: HttpOptions): Promise> { + const method = (options.method || 'GET').toUpperCase() + const url = resolveUrl(options.url) + let data = options.data + if (method === 'GET' && data && typeof data === 'object') { + data = compactQuery(data as Record) + } + const bodyStr = + method === 'GET' || data == null + ? '' + : typeof data === 'string' + ? data + : JSON.stringify(data) + + const header: Record = { + 'Content-Type': 'application/json', + ...(options.header as Record), + ...buildSignHeaders(method, url, data), + } + if (!options.skipAuth) { + const token = String(uni.getStorageSync(TOKEN_KEY) || '') + if (token) header.Token = token + } + + return new Promise((resolve, reject) => { + uni.request({ + ...options, + url, + method: method as UniApp.RequestOptions['method'], + data, + header, + timeout: options.timeout ?? 15000, + success: (res) => { + const body = res.data as ApiResult + if (!body || typeof body.code !== 'number') { + reject(new Error('invalid response')) + return + } + if (body.code === 401 && onUnauthorized) onUnauthorized() + resolve(body) + }, + fail: reject, + }) + }) +} +``` + +## services/request.ts + +```typescript +import { httpRequest } from './http/core' + +export type { ApiResult, PaginateData } from '../types/api' +export { setUnauthorizedHandler, compactQuery, isApiSuccess } from './http/core' + +export function request( + options: Omit & { url: string; skipAuth?: boolean }, +) { + return httpRequest(options) +} +``` + +## 域模块 + +每域必备:`types.ts` `api.ts` `service.ts` `mock.ts` `index.ts`。 + +```typescript +// api.ts — path 与后端控制器一致 +export function apiPost(path: string, data?: unknown, skipAuth = false) { + return request({ url: path, method: 'POST', data, skipAuth }) +} +export function apiGet(path: string, data?: Record, skipAuth = false) { + return request({ url: path, method: 'GET', data, skipAuth }) +} + +// service.ts +import { USE_MOCK } from '@/config' +import { shouldUseRealApi } from '@/config/mock-routes' + +export async function fetchXxx(input: XxxQuery) { + const path = '/api//' + const method = 'GET' + if (USE_MOCK && !shouldUseRealApi(method, path)) return mockXxx(input) + return apiGet(path, input) +} + +// mock.ts — fixture 结构对齐 ApiResult,code 与 VITE_API_SUCCESS_CODE 一致 +// index.ts — export * from './types'; export * from './service' +``` + +客户端校验失败:返回 `{ code: 40001, msg, data: null }`,不发 HTTP。 + +## stores/user.ts + +```typescript +import { defineStore } from 'pinia' + +const TOKEN_KEY = 'token' + +export const useUserStore = defineStore('user', { + state: () => ({ token: '' as string, profile: null as Record | null }), + actions: { + setSession(token: string, profile: Record) { + this.token = token + this.profile = profile + uni.setStorageSync(TOKEN_KEY, token) + }, + logout() { + this.token = '' + this.profile = null + uni.removeStorageSync(TOKEN_KEY) + }, + hydrate() { + this.token = String(uni.getStorageSync(TOKEN_KEY) || '') + }, + }, +}) +``` + +`App.vue`:`onLaunch` → `hydrate()` + `setUnauthorizedHandler(() => userStore.logout())`。 + +## navigation + +```typescript +// core.ts +export function safeNavigate(url: string) { + uni.navigateTo({ url, fail: () => uni.showToast({ title: '跳转失败', icon: 'none' }) }) +} +// routes.ts — 常量路径 +// <域>.ts — goXxx() 封装 +``` + +## 页面 + +主包 Tab/入口;分包重流程。四态 `loading|empty|success|error`。>350 行拆 composable。样式 BEM + scoped scss。 + +## vite.config.ts + +```typescript +import { defineConfig } from 'vite' +import uni from '@dcloudio/vite-plugin-uni' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, + css: { preprocessorOptions: { scss: { additionalData: '@import "@/uni.scss";' } } }, + plugins: [uni()], + server: { + proxy: { + '/api': { target: process.env.VITE_DEV_PROXY_TARGET || 'http://127.0.0.1:8080', changeOrigin: true }, + }, + }, +}) +``` + +H5:`VITE_API_BASE=''` + proxy。小程序/App:完整域名;后端 `fastadmin.cors_request_domain` 放行。 + +## package.json + +```json +{ + "scripts": { + "dev:h5": "uni -p h5", + "dev:mp-weixin": "uni -p mp-weixin", + "build:h5": "uni build -p h5", + "build:mp-weixin": "uni build -p mp-weixin", + "typecheck": "vue-tsc --noEmit" + } +} +``` + +## 初始化顺序 + +```text +1. npx degit dcloudio/uni-preset-vue#vite-ts && cd +2. npm i pinia js-sha256 sass +3. types/api.ts config.ts config/mock-routes.ts +4. services/sign.ts http/core.ts request.ts +5. modules/navigation/* +6. stores/user.ts modules/auth/*(init/login 首域) +7. components/PageShell.vue PageState.vue +8. pages.json easycom tabBar manifest.json uni.scss +9. .env.development .env.production .env.example +10. vite.config.ts main.ts App.vue +11. npm run typecheck && npm run dev:h5 +12. hybrid 联调:REAL_ROUTES 增量;sign/Token 通过后扩域 +``` + +## 新建域 Checklist + +```text +[ ] types.ts +[ ] mock/v1/*.json +[ ] mock.ts api.ts service.ts index.ts +[ ] navigation/<域>.ts + pages.json +[ ] 页面四态 + typecheck +[ ] hybrid 登记 path +``` + +## 禁止 + +- 页面/Store import mock 或 api +- api.ts mock 分支 +- Authorization Bearer(用 Token) +- 自创签名算法 +- production mock 或 secret 入库 +- 页面 uni.request + +## 例外 + +`/static/**` · `uni.login` · `uni.requestPayment` diff --git a/.gitignore b/.gitignore index e3597fc..b75416d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,19 @@ .DS_Store -dist/electron/* -dist/web/* -build/ +Thumbs.db node_modules/ -npm-debug.log -npm-debug.log.* -thumbs.db -!.gitkeep -.backup/ -# 服务器文件存储目录(用户上传的文件) -api/files/ -design/files/ \ No newline at end of file +frontend-electron/node_modules/ +npm-debug.log* +dist/*.zip +_salvage/ +backend-web/config/local.php +docker/.env +docker/config/local.php +docker/config/deploy-config.js +soonModels/*.soon +backend-web/storage/payment/* +!backend-web/storage/**/.gitkeep +*.log +*.tmp +*.bak +*.pem +*.key diff --git a/README.md b/README.md index a4b1328..1b6ed80 100644 --- a/README.md +++ b/README.md @@ -1,361 +1,51 @@ -# SoonDesign 设计工具 +# SoonDesign -专业的卡片和光盘设计软件,支持单面/双面模板设计、二维码/条形码生成、图片编辑等功能。提供**桌面端(Electron)**和**网页端**两种使用方式。 +卡片/光盘设计工具:Electron 桌面端 + Web 端 + PHP 后端。 ---- +## 目录 -## ✨ 主要功能 +| 目录 | 说明 | +|---|---| +| [frontend-electron/](frontend-electron/) | 桌面程序 | +| [frontend-web/](frontend-web/) | Web 前端 | +| [backend-web/](backend-web/) | PHP API | +| [docker/](docker/) | 本地 Docker 编排(非生产部署) | -- 🎨 **模板设计**:支持单面(光盘)和双面(卡片)模板设计 -- 📱 **二维码/条形码**:内置二维码和条形码生成器 -- 🖼️ **图片编辑**:支持图片添加、裁剪、旋转、缩放等操作 -- 📝 **文本编辑**:丰富的文本样式设置(字体、大小、颜色、对齐等) -- 💾 **文件管理**:支持 `.soon` 格式项目文件的保存和打开 -- 🌐 **多端支持**:桌面端(Windows/Mac/Linux)和网页端(现代浏览器) -- 🌍 **多语言**:支持中文简体、中文繁体、英文 +## 本地开发 ---- +需 [Docker Desktop](https://www.docker.com/products/docker-desktop/)。 -## 🚀 快速开始 - -### 系统要求 - -- **Node.js**: >= 16.0.0 -- **npm**: >= 7.0.0 或 **yarn** -- **操作系统**: Windows 7+, macOS 10.12+, Linux (Ubuntu 18.04+) - -### 安装依赖 - -```bash -# 使用 yarn(推荐) -yarn install - -# 或使用 npm -npm install +```bat +docker\sync-config.ps1 :: 同步 API 地址到 deploy-config.js 与 backend local.php(改端口后必跑) +scripts\start.bat :: 启动 +scripts\start.bat stop :: 停止(保留数据库) +scripts\start.bat reset :: 停止并清空数据库卷 ``` -### 启动应用 +`frontend-web/assets/deploy-config.js` 由 `docker/sync-config.ps1` 根据 `docker/.env` 端口生成,请勿手改 localhost 后提交;生产环境在部署时写入实际 API 地址。 -```bash -# 桌面端(Electron) -yarn start +| 服务 | 默认地址 | +|------|----------| +| Web | http://localhost:8100/pages/index.web.html | +| Admin 登录 | http://localhost:8100/pages/admin/login.html | +| Admin 后台 | http://localhost:8100/pages/admin/index.html | +| Electron 静态 | http://localhost:8101/pages/index.html | +| API | http://localhost:8102/health | -# 或 -npm start -``` +端口在 `docker/.env` 修改(默认 8100/8101/8102)。测试管理员:`admin@local.test` / `admin123`(仅 `role=admin` 可进入管理后台)。 -### 网页端使用 +管理后台模块:概览、用户(详情/启停)、订单筛选、套餐、系统设置、审计日志、支付密钥上传。 -#### 方式一:使用本地服务器(推荐)⭐ +桌面窗口(可选):`cd frontend-electron && npm install && npm start` -```bash -# 启动本地服务器(自动打开浏览器) -yarn web +## 生产部署 -# 或使用 npm -npm run web +- Web 根目录:`frontend-web/` +- PHP 根目录:`backend-web/public/` +- 数据库:导入 `backend-web/schema.sql` +- 后端:`backend-web/config/local.php.example` → `local.php` +- 前端 API:`frontend-web/assets/deploy-config.js` -# 开发模式(禁用缓存) -yarn web:dev -``` +支付与 notify 说明见 [docs/PAYMENT.md](docs/PAYMENT.md)。列表分页契约见 [docs/API-PAGINATION.md](docs/API-PAGINATION.md)。 -服务器会在 `http://localhost:8080` 启动,并自动打开首页。 - -> 💡 **提示**:使用本地服务器可以避免 CORS 问题,确保所有资源正确加载。 - -#### 方式二:使用 Python 内置服务器 - -```bash -# Python 3 -python -m http.server 8080 - -# Python 2 -python -m SimpleHTTPServer 8080 -``` - -然后在浏览器中访问 `http://localhost:8080/index.web.html` - -#### 方式三:使用其他静态服务器 - -```bash -# 使用 serve(需要先安装: npm install -g serve) -serve -p 8080 - -# 使用 live-server(需要先安装: npm install -g live-server) -live-server --port=8080 -``` - -#### 方式四:直接打开文件 - -直接在浏览器中打开以下文件: - -- **首页**: `index.web.html` -- **单面设计页**: `design1.web.html` -- **双面设计页**: `design2.web.html` - -> ⚠️ **注意**:直接打开文件时,某些浏览器可能因为安全策略限制文件访问,建议使用方式一。 - -#### 网页端数据存储 - -- **sessionStorage**: 存储当前会话的项目数据(关闭标签页后清空) -- **localStorage**: 存储历史记录和用户设置(持久化) -- **文件格式**: 使用 `soondesign_session:文件名.soon` 作为 session key - -#### 服务器文件保存功能 ⭐ 新增 - -网页端默认将文件保存在浏览器缓存中。如果部署在服务器上,可以启用服务器文件保存功能,实现真正的文件持久化: - -1. **自动检测**:程序会自动检测是否在服务器环境(非 localhost) -2. **自动保存**:保存文件时自动上传到服务器 -3. **自动读取**:打开文件时自动从服务器读取 -4. **降级处理**:如果服务器保存失败,自动降级为浏览器下载 - -**部署步骤**: -- 将 `api` 目录下的 PHP 文件上传到服务器的 `/api/` 目录 -- 创建文件存储目录:`/design/files/` -- 详细说明请参考:[api/README.md](api/README.md) - ---- - -## 📦 构建和发布 - -### 方式一:一键发布 ⭐ 推荐 - -```bash -# Windows 系统 -yarn release:win - -# Mac 系统 -yarn release:mac - -# Linux 系统 -yarn release:linux -``` - -✅ **自动完成**:加密 → 测试 → 清理 → 打包 -✅ **安装包位置**:`build` 文件夹 - -### 方式二:分步操作(手动控制) - -#### 步骤 1:加密代码 - -```bash -yarn encrypt:win # Windows -yarn encrypt:linux # Mac/Linux -``` - -✅ 生成 `.jsc` 加密文件(自动覆盖旧文件) - -#### 步骤 2:测试 - -```bash -yarn start -``` - -✅ 确认程序正常运行,无报错 - -#### 步骤 3:清理源文件 - -```bash -yarn cleanup -``` - -✅ 删除 `.js` 源文件,自动备份到 `.backup` 文件夹 - -#### 步骤 4:打包 - -```bash -yarn build:win # Windows -yarn build:mac # Mac -yarn build:linux # Linux -``` - -✅ 生成安装包到 `build` 文件夹 - ---- - -## 🔧 其他常用命令 - -### 开发相关 - -```bash -# 启动开发模式 -yarn start - -# 构建 Web 资源 -yarn dist - -# 使用 electron-packager 打包(旧方式) -yarn package -``` - -### 文件管理 - -```bash -# 恢复源文件 -yarn restore - -# 清理加密文件 -yarn cleanup:jsc -``` - -> 从 `.backup` 文件夹恢复所有源文件(误删除时使用) - -### 修改文件名映射 - -编辑 `lib/module-map.json`: - -```json -{ - "mappings": { - "design1": { - "core": "core1", // 修改这里 - "output": "output1", - "ui": "ui1" - } - } -} -``` - -然后重新运行 `yarn encrypt:win` - ---- - -## 📁 项目结构 - -``` -soonDesign/ -├── lib/ # 核心代码库 -│ ├── platform/ # 平台抽象层 -│ │ ├── web.js # 网页端实现 -│ │ ├── electron.js # Electron 桌面端实现 -│ │ └── bridge.js # 平台桥接层 -│ ├── common/ # 公共工具 -│ ├── design1/ # 单面模板(光盘) -│ │ ├── core.js # 核心功能 -│ │ ├── output.js # 导出/保存 -│ │ └── ui.js # 界面交互 -│ ├── design2/ # 双面模板(卡片) -│ │ ├── core.js -│ │ ├── output.js -│ │ └── ui.js -│ ├── index.js # 首页入口 -│ ├── design1.js # 单面模板入口 -│ └── design2.js # 双面模板入口 -├── public/ # 静态资源 -│ ├── images/ # 图片资源 -│ └── css/ # 样式文件 -├── layui/ # LayUI 框架 -├── scripts/ # 构建脚本 -│ └── encrypt/ # 加密脚本 -├── help/ # 帮助文档 -├── main.js # Electron 主进程入口 -├── index.web.html # 网页端首页 -├── design1.web.html # 网页端单面设计页 -├── design2.web.html # 网页端双面设计页 -├── package.json # 项目配置 -└── README.md # 本文档 -``` - ---- - -## 🛠️ 技术栈 - -- **框架**: Electron 15.x -- **UI 库**: LayUI -- **画布库**: Fabric.js -- **二维码**: jr-qrcode -- **条形码**: jsbarcode -- **构建工具**: electron-builder, webpack -- **语言**: JavaScript (ES5+) - ---- - -## 🌐 网页端特性 - -### 存储机制 - -- **sessionStorage**: 存储当前会话的项目数据(关闭标签页后清空) -- **localStorage**: 存储历史记录和用户设置(持久化) -- **文件格式**: 使用 `soondesign_session:文件名.soon` 作为 session key - -### 功能限制 - -- 文件保存会触发浏览器下载(使用 File System Access API 时除外) -- 关闭标签页后 session 数据会丢失,但历史记录会保留 -- 部分浏览器可能限制脚本关闭页签的功能 - -### 浏览器兼容性 - -- Chrome/Edge 90+ -- Firefox 88+ -- Safari 14+ - ---- - -## ⚠️ 注意事项 - -### 开发时 - -- **保留 `.js` 源文件**,不要运行 `cleanup` -- 使用 `yarn start` 启动开发模式 -- 按 `F12` 打开开发者工具调试 - -### 发布时 - -- **必须先加密、再清理、最后打包** -- 备份文件在 `.backup` 文件夹(不会被打包进应用) -- 确保所有依赖已正确安装 - -### 网页端 - -- 数据存储在浏览器本地,清除浏览器数据会丢失历史记录 -- 建议定期导出 `.soon` 文件作为备份 -- 不同域名下的数据不共享 - ---- - -## 🔧 调试模式 - -### 桌面端 - -按 `F12` 或 `Ctrl+Shift+I`(Mac: `Cmd+Option+I`)打开开发者工具查看错误信息。 - -### 网页端 - -同样使用浏览器开发者工具(F12),查看 Console 面板的日志和错误信息。 - ---- - -## 📚 更多文档 - -- [加密详细说明](scripts/encrypt/README.md) -- [网页端功能清单](docs/网页端功能清单.md) - ---- - -## 📝 版本信息 - -- **当前版本**: v3.2.101 -- **更新日期**: 2026-02-07 - ---- - -## 📄 许可证 - -MIT License - -Copyright © 2023 [CardSoon](https://www.cardsoon.com) - ---- - -## 🤝 贡献 - -欢迎提交 Issue 和 Pull Request! - ---- - -## 📞 联系方式 - -- **官网**: [CardSoon](https://www.cardsoon.com/index.php/cn/products/cardsoonsoft.html) -- **邮箱**: jerry79.xu@gmail.com +`frontend-electron/js/design1-back.js`、`design2-back.js` 等为历史参考快照,页面勿引用;功能对齐请以当前 `frontend-web` / `frontend-electron` 为准。 diff --git a/api/README.md b/api/README.md deleted file mode 100644 index 3d405cc..0000000 --- a/api/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# SoonDesign 网页端服务器文件保存 API - -## 功能说明 - -网页端默认将文件保存在浏览器缓存(localStorage/sessionStorage)中。通过部署这些 API 接口,可以实现将文件保存到服务器,实现真正的文件持久化。 - -## 部署步骤 - -### 1. 上传 API 文件 - -将 `api` 目录下的 PHP 文件上传到服务器的 `/api/` 目录(相对于网站根目录)。 - -例如,如果网站根目录是 `/www/wwwroot/soonWebsite/`,则: -- `save_file.php` → `/www/wwwroot/soonWebsite/api/save_file.php` -- `read_file.php` → `/www/wwwroot/soonWebsite/api/read_file.php` - -### 2. 创建文件存储目录 - -创建用于存储 `.soon` 文件的目录: - -```bash -mkdir -p /www/wwwroot/soonWebsite/design/files -chmod 755 /www/wwwroot/soonWebsite/design/files -``` - -### 3. 配置 Nginx(如果需要) - -确保 Nginx 配置允许访问 `/api/` 和 `/design/files/` 目录: - -```nginx -location /api/ { - try_files $uri $uri/ =404; -} - -location /design/files/ { - alias /www/wwwroot/soonWebsite/design/files/; - # 允许列出文件(可选) - autoindex off; -} -``` - -### 4. 测试 API - -访问以下 URL 测试 API 是否正常工作: - -- 保存文件:`http://114.55.133.123:8888/api/save_file.php` -- 读取文件:`http://114.55.133.123:8888/api/read_file.php?fileName=test.soon` - -## 工作原理 - -1. **自动检测服务器环境**:网页端会自动检测是否在服务器环境(非 localhost/127.0.0.1) -2. **保存文件**:当用户保存文件时,如果是服务器环境,会自动调用 `save_file.php` 将文件上传到服务器 -3. **读取文件**:当打开文件时,如果是服务器文件路径,会自动调用 `read_file.php` 从服务器读取 -4. **降级处理**:如果服务器保存失败,会自动降级为浏览器下载 - -## 文件存储位置 - -所有保存的 `.soon` 文件存储在: -``` -/design/files/ -``` - -文件访问路径为: -``` -http://114.55.133.123:8888/design/files/文件名.soon -``` - -## 安全说明 - -- API 接口已包含文件名清理,防止路径遍历攻击 -- 建议在生产环境中添加身份验证(如 Token 验证) -- 建议限制文件大小和类型 -- 建议定期清理旧文件 - -## 故障排除 - -### 问题:保存时提示"保存失败" - -**可能原因:** -1. API 文件路径不正确 -2. 文件存储目录权限不足 -3. PHP 配置问题 - -**解决方法:** -1. 检查 API 文件是否在正确位置 -2. 检查目录权限:`chmod 755 /www/wwwroot/soonWebsite/design/files` -3. 检查 PHP 错误日志 - -### 问题:文件保存后找不到 - -**可能原因:** -1. 文件保存路径配置错误 -2. Nginx 配置问题 - -**解决方法:** -1. 检查 `save_file.php` 中的 `$saveDir` 路径 -2. 检查 Nginx 配置是否正确指向文件目录 diff --git a/api/read_file.php b/api/read_file.php deleted file mode 100644 index b655b4d..0000000 --- a/api/read_file.php +++ /dev/null @@ -1,75 +0,0 @@ - false, 'error' => 'Method not allowed']); - exit; -} - -// 获取文件名 -$fileName = isset($_GET['fileName']) ? $_GET['fileName'] : ''; - -// 验证文件名 -if (empty($fileName)) { - http_response_code(400); - echo json_encode(['success' => false, 'error' => '文件名不能为空']); - exit; -} - -// 清理文件名,防止路径遍历攻击 -$fileName = basename($fileName); -if (strpos($fileName, '..') !== false) { - http_response_code(400); - echo json_encode(['success' => false, 'error' => '无效的文件名']); - exit; -} - -// 获取文件路径 -$saveDir = dirname(__DIR__) . '/design/files/'; -$filePath = $saveDir . $fileName; - -// 检查文件是否存在 -if (!file_exists($filePath)) { - http_response_code(404); - echo json_encode(['success' => false, 'error' => '文件不存在']); - exit; -} - -// 读取文件内容 -try { - $content = file_get_contents($filePath); - if ($content === false) { - throw new Exception('文件读取失败'); - } - - // 尝试解析 JSON 验证格式 - $json = json_decode($content, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception('文件格式错误: ' . json_last_error_msg()); - } - - echo $content; -} catch (Exception $e) { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => '读取失败: ' . $e->getMessage() - ]); -} -?> diff --git a/api/save_file.php b/api/save_file.php deleted file mode 100644 index c22af92..0000000 --- a/api/save_file.php +++ /dev/null @@ -1,88 +0,0 @@ - false, 'error' => 'Method not allowed']); - exit; -} - -// 获取保存目录(相对于网站根目录) -// 如果 API 在 /api/ 目录,文件存储在 /design/files/ -$saveDir = dirname(__DIR__) . '/design/files/'; -// 如果上述路径不正确,请根据实际部署情况修改,例如: -// $saveDir = '/www/wwwroot/soonWebsite/design/files/'; - -// 确保目录存在 -if (!is_dir($saveDir)) { - mkdir($saveDir, 0755, true); -} - -// 获取文件名和内容 -$fileName = isset($_POST['fileName']) ? $_POST['fileName'] : ''; -$fileContent = isset($_POST['fileContent']) ? $_POST['fileContent'] : ''; - -// 验证文件名 -if (empty($fileName)) { - http_response_code(400); - echo json_encode(['success' => false, 'error' => '文件名不能为空']); - exit; -} - -// 清理文件名,防止路径遍历攻击 -$fileName = basename($fileName); -if (strpos($fileName, '..') !== false) { - http_response_code(400); - echo json_encode(['success' => false, 'error' => '无效的文件名']); - exit; -} - -// 确保文件名以 .soon 结尾 -if (substr($fileName, -5) !== '.soon') { - $fileName = preg_replace('/\.soon$/i', '', $fileName) . '.soon'; -} - -// 验证内容 -if (empty($fileContent)) { - http_response_code(400); - echo json_encode(['success' => false, 'error' => '文件内容不能为空']); - exit; -} - -// 保存文件 -$filePath = $saveDir . $fileName; -try { - $result = file_put_contents($filePath, $fileContent); - if ($result === false) { - throw new Exception('文件写入失败'); - } - - echo json_encode([ - 'success' => true, - 'message' => '文件保存成功', - 'filePath' => '/design/files/' . $fileName, - 'fileName' => $fileName - ]); -} catch (Exception $e) { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => '保存失败: ' . $e->getMessage() - ]); -} -?> diff --git a/backend-web/config/config.design.cardsoon.example.php b/backend-web/config/config.design.cardsoon.example.php new file mode 100644 index 0000000..494c46a --- /dev/null +++ b/backend-web/config/config.design.cardsoon.example.php @@ -0,0 +1,25 @@ + [ + 'env' => 'production', + 'debug' => false, + 'cors_origins' => [ + 'https://design.cardsoon.com', + ], + ], + 'db' => [ + 'host' => '127.0.0.1', + 'port' => 3306, + 'name' => 'soondesign', + 'user' => 'soondesign', + 'pass' => 'CHANGE-ME', + ], + 'site' => [ + 'base_url' => 'https://design.cardsoon.com', + 'front_base_url' => 'https://design.cardsoon.com', + ], + 'storage' => [ + 'users_dir' => '/www/wwwroot/designadmin.cardsoon.com/backend/storage/users', + ], +]; diff --git a/backend-web/config/config.designadmin.cardsoon.example.php b/backend-web/config/config.designadmin.cardsoon.example.php new file mode 100644 index 0000000..4a05439 --- /dev/null +++ b/backend-web/config/config.designadmin.cardsoon.example.php @@ -0,0 +1,25 @@ + [ + 'env' => 'production', + 'debug' => false, + 'cors_origins' => [ + 'https://design.cardsoon.com', + ], + ], + 'db' => [ + 'host' => '127.0.0.1', + 'port' => 3306, + 'name' => 'soondesign', + 'user' => 'soondesign', + 'pass' => 'CHANGE-ME', + ], + 'site' => [ + 'base_url' => 'https://designadmin.cardsoon.com', + 'front_base_url' => 'https://design.cardsoon.com', + ], + 'storage' => [ + 'users_dir' => '/www/wwwroot/designadmin.cardsoon.com/backend/storage/users', + ], +]; diff --git a/backend-web/config/local.php.example b/backend-web/config/local.php.example new file mode 100644 index 0000000..c02590c --- /dev/null +++ b/backend-web/config/local.php.example @@ -0,0 +1,57 @@ + [ + 'env' => 'production', + 'debug' => false, + 'timezone' => 'Asia/Shanghai', + 'jwt_secret' => 'CHANGE-ME-32CHARS-MINIMUM-SECRET', + 'jwt_ttl' => 3600, + 'jwt_refresh_ttl' => 2592000, + 'cors_origins' => [ + 'https://design.cardsoon.com', + ], + ], + 'db' => [ + 'host' => '127.0.0.1', + 'port' => 3306, + 'name' => 'soondesign', + 'user' => 'soondesign', + 'pass' => 'CHANGE-ME', + 'charset' => 'utf8mb4', + ], + 'site' => [ + 'base_url' => 'https://designadmin.cardsoon.com', + 'front_base_url' => 'https://design.cardsoon.com', + ], + 'storage' => [ + 'users_dir' => '/www/wwwroot/designadmin.cardsoon.com/backend/storage/users', + 'models_dir' => '/www/wwwroot/design.cardsoon.com/soonModels', + ], + 'limits' => [ + 'free_quota_mb' => 20, + 'preview_require_membership' => true, + 'member_quota_mb' => 2048, + 'member_max_files' => 200, + ], + 'rate_limits' => [ + 'admin/*' => ['capacity' => 120, 'window' => 60], + 'POST /api/v1/auth/login' => ['capacity' => 10, 'window' => 60], + 'POST /api/v1/auth/register' => ['capacity' => 5, 'window' => 60], + 'POST /api/v1/files' => ['capacity' => 30, 'window' => 60], + ], + 'alipay' => [ + 'app_id' => '', + 'private_key' => '', + 'public_key' => '', + 'sandbox' => false, + ], + 'wechat' => [ + 'app_id' => '', + 'mch_id' => '', + 'mch_serial_no' => '', + 'mch_private_key' => '', + 'api_v3_key' => '', + 'sandbox' => false, + ], +]; diff --git a/backend-web/php-upload.example.ini b/backend-web/php-upload.example.ini new file mode 100644 index 0000000..c5edfdc --- /dev/null +++ b/backend-web/php-upload.example.ini @@ -0,0 +1,5 @@ +; PHP-FPM 与 PHP-CLI 都建议同步: +upload_max_filesize = 50M +post_max_size = 50M +memory_limit = 256M +max_execution_time = 60 diff --git a/backend-web/public/admin.php b/backend-web/public/admin.php new file mode 100644 index 0000000..b2dc5dd --- /dev/null +++ b/backend-web/public/admin.php @@ -0,0 +1,93 @@ + $v) { + if ($k === 'id' && is_string($v) && ctype_digit($v)) { + $params[$k] = (int)$v; + } +} +if (is_array($handler)) { + [$class, $action] = $handler; + $instance = new $class(); + $args = array_values($params); + $instance->$action($adminId, ...$args); +} else { + ($handler)($params); +} diff --git a/backend-web/public/index.php b/backend-web/public/index.php new file mode 100644 index 0000000..019e8cb --- /dev/null +++ b/backend-web/public/index.php @@ -0,0 +1,101 @@ + $v) { + if ($k === 'id' && is_string($v) && ctype_digit($v)) { + $params[$k] = (int)$v; + } +} +if (is_array($handler)) { + [$class, $action] = $handler; + $instance = new $class(); + $args = array_values($params); + $instance->$action(...$args); +} else { + ($handler)($params); +} diff --git a/backend-web/schema.sql b/backend-web/schema.sql new file mode 100644 index 0000000..f6c25fe --- /dev/null +++ b/backend-web/schema.sql @@ -0,0 +1,126 @@ +-- SoonDesign 数据库结构 (UTF-8 无 BOM) +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `email` VARCHAR(190) NOT NULL, + `password_hash` VARCHAR(255) NOT NULL, + `role` ENUM('user','admin') NOT NULL DEFAULT 'user', + `admin_level` ENUM('full','ops') DEFAULT NULL, + `status` ENUM('active','disabled') NOT NULL DEFAULT 'active', + `created_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `soon_files` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(190) NOT NULL, + `json` LONGTEXT NOT NULL, + `size` INT UNSIGNED NOT NULL DEFAULT 0, + `version` INT UNSIGNED NOT NULL DEFAULT 1, + `created_at` DATETIME NOT NULL, + `updated_at` DATETIME NOT NULL, + `deleted_at` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user` (`user_id`, `deleted_at`, `updated_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `plans` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `code` VARCHAR(64) NOT NULL, + `name` VARCHAR(120) NOT NULL, + `description` VARCHAR(255) DEFAULT NULL, + `price_cents` INT UNSIGNED NOT NULL, + `quota_mb` INT UNSIGNED NOT NULL, + `max_files` INT UNSIGNED NOT NULL DEFAULT 0, + `duration_days` INT UNSIGNED NOT NULL, + `features` TEXT DEFAULT NULL, + `sort_order` INT NOT NULL DEFAULT 0, + `is_recommended` TINYINT(1) NOT NULL DEFAULT 0, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `code` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `plans` (`code`,`name`,`description`,`price_cents`,`quota_mb`,`max_files`,`duration_days`,`features`,`sort_order`,`is_recommended`,`is_active`) VALUES + ('free','免费版','免费体验设计与编辑',0,2048,200,0, + '["设计与编辑工具免费使用"]',0,0,1), + ('member_monthly','月度订阅','按月灵活使用,随时续订',1999,2048,200,30, + '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]',10,1,1), + ('member_quarterly','季度订阅','连续三个月,更省心',5299,2048,200,90, + '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]',15,0,1), + ('member_yearly','年度订阅','全年畅享,性价比更高',19999,2048,200,365, + '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]',20,0,1) +ON DUPLICATE KEY UPDATE + name=VALUES(name), + description=VALUES(description), + price_cents=VALUES(price_cents), + quota_mb=VALUES(quota_mb), + max_files=VALUES(max_files), + duration_days=VALUES(duration_days), + features=VALUES(features), + sort_order=VALUES(sort_order), + is_recommended=VALUES(is_recommended); + +CREATE TABLE IF NOT EXISTS `subscriptions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` INT UNSIGNED NOT NULL, + `plan_id` INT UNSIGNED NOT NULL, + `source_order_id` BIGINT UNSIGNED DEFAULT NULL, + `status` ENUM('active','expired','cancelled') NOT NULL DEFAULT 'active', + `started_at` DATETIME NOT NULL, + `expires_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `user` (`user_id`, `status`, `expires_at`), + KEY `source_order` (`source_order_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `pay_orders` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `order_no` VARCHAR(64) NOT NULL, + `user_id` INT UNSIGNED NOT NULL, + `plan_id` INT UNSIGNED NOT NULL, + `channel` ENUM('alipay','wechat') NOT NULL, + `amount_cents` INT UNSIGNED NOT NULL, + `status` ENUM('pending','paid','cancelled','refunded') NOT NULL DEFAULT 'pending', + `refund_status` ENUM('none','pending','approved','rejected') NOT NULL DEFAULT 'none', + `refund_reason` TEXT DEFAULT NULL, + `refund_note` TEXT DEFAULT NULL, + `txn_id` VARCHAR(120) DEFAULT NULL, + `client_ip` VARCHAR(64) DEFAULT NULL, + `created_at` DATETIME NOT NULL, + `paid_at` DATETIME DEFAULT NULL, + `cancelled_at` DATETIME DEFAULT NULL, + `refunded_at` DATETIME DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `order_no` (`order_no`), + KEY `user` (`user_id`), + KEY `status` (`status`), + KEY `refund_status` (`refund_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `audit_logs` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `admin_id` INT UNSIGNED NOT NULL, + `action` VARCHAR(120) NOT NULL, + `target` VARCHAR(190) NOT NULL, + `context` TEXT, + `ip` VARCHAR(64) DEFAULT NULL, + `created_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `admin` (`admin_id`, `created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `settings` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `key` VARCHAR(120) NOT NULL, + `value` TEXT NOT NULL, + `updated_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `k` (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/backend-web/src/Admin/Controllers/AuditsController.php b/backend-web/src/Admin/Controllers/AuditsController.php new file mode 100644 index 0000000..ef4257c --- /dev/null +++ b/backend-web/src/Admin/Controllers/AuditsController.php @@ -0,0 +1,74 @@ +} */ + private function buildFilters(): array + { + $where = '1=1'; + $params = []; + $q = trim((string)($_GET['q'] ?? '')); + if ($q !== '') { + $like = '%' . $q . '%'; + $where .= ' AND (a.action LIKE :q_action OR a.target LIKE :q_target OR u.email LIKE :q_email)'; + $params['q_action'] = $like; + $params['q_target'] = $like; + $params['q_email'] = $like; + } + $action = trim((string)($_GET['action'] ?? '')); + if ($action !== '') { + $where .= ' AND a.action = :action'; + $params['action'] = $action; + } + $from = trim((string)($_GET['from'] ?? '')); + if ($from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) { + $where .= ' AND a.created_at >= :from_dt'; + $params['from_dt'] = $from . ' 00:00:00'; + } + $to = trim((string)($_GET['to'] ?? '')); + if ($to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) { + $where .= ' AND a.created_at <= :to_dt'; + $params['to_dt'] = $to . ' 23:59:59'; + } + return ['where' => $where, 'params' => $params]; + } + + public function list(int $adminId): void + { + $page = max(1, (int)($_GET['page'] ?? 1)); + $size = max(1, min(200, (int)($_GET['size'] ?? 50))); + $offset = ($page - 1) * $size; + $filters = $this->buildFilters(); + $where = $filters['where']; + $params = $filters['params']; + + $pdo = Db::pdo(); + $sql = 'SELECT a.*, u.email AS admin_email FROM audit_logs a ' + . 'LEFT JOIN users u ON u.id = a.admin_id ' + . 'WHERE ' . $where . ' ORDER BY a.id DESC LIMIT :lim OFFSET :off'; + $stmt = $pdo->prepare($sql); + foreach ($params as $k => $v) { + $stmt->bindValue($k, $v); + } + $stmt->bindValue('lim', $size, \PDO::PARAM_INT); + $stmt->bindValue('off', $offset, \PDO::PARAM_INT); + $stmt->execute(); + + $countStmt = $pdo->prepare( + 'SELECT COUNT(*) FROM audit_logs a LEFT JOIN users u ON u.id = a.admin_id WHERE ' . $where + ); + foreach ($params as $k => $v) { + $countStmt->bindValue($k, $v); + } + $countStmt->execute(); + $count = (int)$countStmt->fetchColumn(); + + Json::ok(['items' => $stmt->fetchAll(), 'total' => $count, 'page' => $page, 'size' => $size]); + } +} diff --git a/backend-web/src/Admin/Controllers/OrdersController.php b/backend-web/src/Admin/Controllers/OrdersController.php new file mode 100644 index 0000000..fcf8ad3 --- /dev/null +++ b/backend-web/src/Admin/Controllers/OrdersController.php @@ -0,0 +1,147 @@ +} */ + private function buildFilters(): array + { + $where = '1=1'; + $params = []; + $q = trim((string)($_GET['q'] ?? '')); + if ($q !== '') { + $like = '%' . $q . '%'; + $where .= ' AND (u.email LIKE :q_email OR o.order_no LIKE :q_order)'; + $params['q_email'] = $like; + $params['q_order'] = $like; + } + $status = trim((string)($_GET['status'] ?? '')); + if (in_array($status, ['pending', 'paid', 'cancelled', 'refunded'], true)) { + $where .= ' AND o.status = :status'; + $params['status'] = $status; + } + $refundStatus = trim((string)($_GET['refund_status'] ?? '')); + if (in_array($refundStatus, ['none', 'pending', 'approved', 'rejected'], true)) { + $where .= ' AND o.refund_status = :refund_status'; + $params['refund_status'] = $refundStatus; + } + $channel = trim((string)($_GET['channel'] ?? '')); + if (in_array($channel, ['alipay', 'wechat'], true)) { + $where .= ' AND o.channel = :channel'; + $params['channel'] = $channel; + } + $from = trim((string)($_GET['from'] ?? '')); + if ($from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) { + $where .= ' AND o.created_at >= :from_dt'; + $params['from_dt'] = $from . ' 00:00:00'; + } + $to = trim((string)($_GET['to'] ?? '')); + if ($to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) { + $where .= ' AND o.created_at <= :to_dt'; + $params['to_dt'] = $to . ' 23:59:59'; + } + return ['where' => $where, 'params' => $params]; + } + + public function list(int $adminId): void + { + $page = max(1, (int)($_GET['page'] ?? 1)); + $size = max(1, min(200, (int)($_GET['size'] ?? 20))); + $offset = ($page - 1) * $size; + $filters = $this->buildFilters(); + $where = $filters['where']; + $params = $filters['params']; + + $pdo = Db::pdo(); + $sql = 'SELECT o.id, o.order_no, o.user_id, o.plan_id, o.channel, o.amount_cents, o.status, ' + . 'o.refund_status, o.refund_reason, o.created_at, o.paid_at, o.cancelled_at, o.refunded_at, ' + . 'u.email, p.name AS plan_name ' + . 'FROM pay_orders o ' + . 'LEFT JOIN users u ON u.id = o.user_id ' + . 'LEFT JOIN plans p ON p.id = o.plan_id ' + . 'WHERE ' . $where . ' ORDER BY o.id DESC LIMIT :lim OFFSET :off'; + $stmt = $pdo->prepare($sql); + foreach ($params as $k => $v) { + $stmt->bindValue($k, $v); + } + $stmt->bindValue('lim', $size, \PDO::PARAM_INT); + $stmt->bindValue('off', $offset, \PDO::PARAM_INT); + $stmt->execute(); + + $countStmt = $pdo->prepare( + 'SELECT COUNT(*) FROM pay_orders o LEFT JOIN users u ON u.id = o.user_id WHERE ' . $where + ); + foreach ($params as $k => $v) { + $countStmt->bindValue($k, $v); + } + $countStmt->execute(); + $count = (int)$countStmt->fetchColumn(); + + AuditService::log($adminId, 'orders.list', 'pay_orders', ['page' => $page, 'filters' => $params]); + Json::ok(['items' => $stmt->fetchAll(), 'total' => $count, 'page' => $page, 'size' => $size]); + } + + public function detail(int $adminId, int $id): void + { + $pdo = Db::pdo(); + $stmt = $pdo->prepare( + 'SELECT o.*, u.email, p.name AS plan_name, p.code AS plan_code ' + . 'FROM pay_orders o ' + . 'LEFT JOIN users u ON u.id = o.user_id ' + . 'LEFT JOIN plans p ON p.id = o.plan_id ' + . 'WHERE o.id = :id' + ); + $stmt->execute(['id' => $id]); + $order = $stmt->fetch(); + if (!$order) { + Json::fail('not_found', '订单不存在', 404); + } + + $subStmt = $pdo->prepare( + 'SELECT s.id, s.status, s.started_at, s.expires_at FROM subscriptions s ' + . 'WHERE s.source_order_id = :oid ORDER BY s.id DESC LIMIT 1' + ); + $subStmt->execute(['oid' => $id]); + $subscription = $subStmt->fetch() ?: null; + + AuditService::log($adminId, 'orders.detail', 'pay_order:' . $id); + Json::ok(['order' => $order, 'subscription' => $subscription]); + } + + public function cancel(int $adminId, int $id): void + { + if (!PayService::cancelPending($id)) { + Json::fail('bad_request', '仅待支付订单可取消', 400); + } + AuditService::log($adminId, 'orders.cancel', 'pay_order:' . $id); + Json::ok(['id' => $id, 'status' => 'cancelled']); + } + + public function refundApprove(int $adminId, int $id): void + { + $body = Json::readBody(); + $note = trim((string)($body['note'] ?? '')); + $result = PayService::approveRefund($id, $note !== '' ? $note : null); + AuditService::log($adminId, 'orders.refundApprove', 'pay_order:' . $id, [ + 'gateway' => $result['gateway'], + 'note' => $note, + ]); + Json::ok(['id' => $id, 'status' => 'refunded', 'gateway' => $result['gateway']]); + } + + public function refundReject(int $adminId, int $id): void + { + $body = Json::readBody(); + $note = trim((string)($body['note'] ?? '')); + PayService::rejectRefund($id, $note); + AuditService::log($adminId, 'orders.refundReject', 'pay_order:' . $id, ['note' => $note]); + Json::ok(['id' => $id, 'refund_status' => 'rejected']); + } +} diff --git a/backend-web/src/Admin/Controllers/PaymentController.php b/backend-web/src/Admin/Controllers/PaymentController.php new file mode 100644 index 0000000..b26b205 --- /dev/null +++ b/backend-web/src/Admin/Controllers/PaymentController.php @@ -0,0 +1,27 @@ +query( + 'SELECT id, code, name, description, price_cents, quota_mb, max_files, duration_days, features, ' + . 'sort_order, is_recommended, is_active FROM plans ORDER BY sort_order ASC, price_cents ASC' + ); + $items = []; + foreach ($stmt->fetchAll() as $row) { + $plan = MembershipService::enrichPlanForAdmin($row); + $plan['features_editable'] = MembershipService::coreFeatureLines($plan['features']); + $items[] = $plan; + } + AuditService::log($adminId, 'plans.list', 'plans'); + Json::ok(['items' => $items]); + } + + public function upsert(int $adminId): void + { + $body = Json::readBody(); + $code = trim((string)($body['code'] ?? '')); + $name = trim((string)($body['name'] ?? '')); + $description = trim((string)($body['description'] ?? '')); + $priceCents = (int)($body['price_cents'] ?? 0); + $quotaMb = (int)($body['quota_mb'] ?? 0); + $maxFiles = (int)($body['max_files'] ?? 0); + $durationDays = (int)($body['duration_days'] ?? 0); + $sortOrder = (int)($body['sort_order'] ?? 0); + $isRecommended = (int)($body['is_recommended'] ?? 0) === 1 ? 1 : 0; + $isActive = (int)($body['is_active'] ?? 1) === 1 ? 1 : 0; + $syncMemberBenefits = (int)($body['sync_member_benefits'] ?? 0) === 1; + $coreFeatures = MembershipService::sanitizeFeatureLines($body['features'] ?? []); + + if ($code === '' || !preg_match('/^[a-z][a-z0-9_]{1,62}$/', $code)) { + Json::fail('bad_request', 'code 须为小写字母开头的 2~63 位标识', 400); + } + if ($name === '') { + Json::fail('bad_request', 'name 必填', 400); + } + if ($priceCents < 0 || $quotaMb <= 0 || $maxFiles <= 0) { + Json::fail('bad_request', '价格不能为负,配额与文件数须大于 0', 400); + } + if ($code === 'free' && $priceCents > 0) { + Json::fail('bad_request', '免费版价格须为 0', 400); + } + if ($code !== 'free' && $priceCents <= 0) { + Json::fail('bad_request', '付费套餐价格须大于 0', 400); + } + if ($code !== 'free' && $durationDays <= 0) { + Json::fail('bad_request', '付费套餐须设置有效天数', 400); + } + if ($coreFeatures === []) { + Json::fail('bad_request', '请至少填写一条权益说明', 400); + } + + $pdo = Db::pdo(); + $features = json_encode( + MembershipService::buildPlanFeatures($code, $durationDays, $coreFeatures), + JSON_UNESCAPED_UNICODE + ); + + if ($isRecommended === 1) { + $pdo->exec('UPDATE plans SET is_recommended = 0'); + } + + $stmt = $pdo->prepare( + 'INSERT INTO plans (code, name, description, price_cents, quota_mb, max_files, duration_days, ' + . 'features, sort_order, is_recommended, is_active) ' + . 'VALUES (:c, :n, :d, :p, :q, :mf, :dd, :f, :so, :ir, :a) ' + . 'ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), ' + . 'price_cents=VALUES(price_cents), quota_mb=VALUES(quota_mb), max_files=VALUES(max_files), ' + . 'duration_days=VALUES(duration_days), features=VALUES(features), ' + . 'sort_order=VALUES(sort_order), is_recommended=VALUES(is_recommended), is_active=VALUES(is_active)' + ); + $stmt->execute([ + 'c' => $code, + 'n' => $name, + 'd' => $description !== '' ? $description : null, + 'p' => $priceCents, + 'q' => $quotaMb, + 'mf' => $maxFiles, + 'dd' => $durationDays, + 'f' => $features, + 'so' => $sortOrder, + 'ir' => $isRecommended, + 'a' => $isActive, + ]); + + if ($syncMemberBenefits && $code !== 'free') { + $paidStmt = $pdo->query( + 'SELECT code, duration_days FROM plans WHERE code <> "free" AND price_cents > 0' + ); + $syncStmt = $pdo->prepare('UPDATE plans SET features = :f WHERE code = :c'); + foreach ($paidStmt->fetchAll() as $paid) { + $paidCode = (string)$paid['code']; + $paidDays = (int)$paid['duration_days']; + $syncStmt->execute([ + 'f' => json_encode( + MembershipService::buildPlanFeatures($paidCode, $paidDays, $coreFeatures), + JSON_UNESCAPED_UNICODE + ), + 'c' => $paidCode, + ]); + } + } + + AuditService::log($adminId, 'plans.upsert', 'plan:' . $code, $body); + Json::ok(['code' => $code]); + } +} diff --git a/backend-web/src/Admin/Controllers/SettingsController.php b/backend-web/src/Admin/Controllers/SettingsController.php new file mode 100644 index 0000000..f5d4021 --- /dev/null +++ b/backend-web/src/Admin/Controllers/SettingsController.php @@ -0,0 +1,68 @@ +query('SELECT `key`, `value`, updated_at FROM settings ORDER BY `key`'); + AuditService::log($adminId, 'settings.list', 'settings'); + Json::ok(['items' => $stmt->fetchAll()]); + } + + public function set(int $adminId): void + { + $body = Json::readBody(); + $key = (string)($body['key'] ?? ''); + $value = (string)($body['value'] ?? ''); + if ($key === '') { + Json::fail('bad_request', 'key 必填', 400); + } + $stmt = Db::pdo()->prepare( + 'INSERT INTO settings (`key`, `value`, updated_at) VALUES (:k, :v, :ts) ' + . 'ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), updated_at=VALUES(updated_at)' + ); + $stmt->execute(['k' => $key, 'v' => $value, 'ts' => date('Y-m-d H:i:s')]); + AuditService::log($adminId, 'settings.set', 'setting:' . $key, ['value' => $value]); + Json::ok(['key' => $key]); + } + + public function setBatch(int $adminId): void + { + $body = Json::readBody(); + $items = $body['items'] ?? null; + if (!is_array($items) || $items === []) { + Json::fail('bad_request', 'items 须为非空数组', 400); + } + $pdo = Db::pdo(); + $stmt = $pdo->prepare( + 'INSERT INTO settings (`key`, `value`, updated_at) VALUES (:k, :v, :ts) ' + . 'ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), updated_at=VALUES(updated_at)' + ); + $saved = []; + $now = date('Y-m-d H:i:s'); + foreach ($items as $item) { + if (!is_array($item)) { + continue; + } + $key = trim((string)($item['key'] ?? '')); + if ($key === '') { + continue; + } + $value = (string)($item['value'] ?? ''); + $stmt->execute(['k' => $key, 'v' => $value, 'ts' => $now]); + $saved[] = $key; + } + if ($saved === []) { + Json::fail('bad_request', '无有效配置项', 400); + } + AuditService::log($adminId, 'settings.set_batch', 'settings', ['keys' => $saved]); + Json::ok(['keys' => $saved]); + } +} diff --git a/backend-web/src/Admin/Controllers/StatsController.php b/backend-web/src/Admin/Controllers/StatsController.php new file mode 100644 index 0000000..428e7da --- /dev/null +++ b/backend-web/src/Admin/Controllers/StatsController.php @@ -0,0 +1,48 @@ +query( + 'SELECT o.id, o.order_no, o.amount_cents, o.status, o.created_at, u.email ' + . 'FROM pay_orders o LEFT JOIN users u ON u.id = o.user_id ' + . 'ORDER BY o.id DESC LIMIT 5' + ); + + Json::ok([ + 'users' => (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(), + 'orders' => (int)$pdo->query('SELECT COUNT(*) FROM pay_orders')->fetchColumn(), + 'orders_today' => (int)$pdo->query( + "SELECT COUNT(*) FROM pay_orders WHERE DATE(created_at) = CURDATE()" + )->fetchColumn(), + 'orders_pending' => (int)$pdo->query( + "SELECT COUNT(*) FROM pay_orders WHERE status = 'pending'" + )->fetchColumn(), + 'refunds_pending' => (int)$pdo->query( + "SELECT COUNT(*) FROM pay_orders WHERE refund_status = 'pending'" + )->fetchColumn(), + 'active_subscriptions' => (int)$pdo->query( + "SELECT COUNT(*) FROM subscriptions WHERE status = 'active'" + )->fetchColumn(), + 'files_total' => (int)$pdo->query( + 'SELECT COUNT(*) FROM soon_files WHERE deleted_at IS NULL' + )->fetchColumn(), + 'revenue_today_cents' => (int)$pdo->query( + "SELECT COALESCE(SUM(amount_cents), 0) FROM pay_orders " + . "WHERE status = 'paid' AND paid_at IS NOT NULL AND DATE(paid_at) = CURDATE()" + )->fetchColumn(), + 'recent_orders' => $recentStmt->fetchAll(), + ]); + } +} diff --git a/backend-web/src/Admin/Controllers/UsersController.php b/backend-web/src/Admin/Controllers/UsersController.php new file mode 100644 index 0000000..553bb19 --- /dev/null +++ b/backend-web/src/Admin/Controllers/UsersController.php @@ -0,0 +1,335 @@ + NOW() ' + . 'ORDER BY s.expires_at DESC LIMIT 1) AS plan_name ' + . 'FROM users u'; + $pdo = Db::pdo(); + if ($q !== '') { + $like = '%' . $q . '%'; + $stmt = $pdo->prepare($baseSql . ' WHERE u.email LIKE :q ORDER BY u.id DESC LIMIT :lim OFFSET :off'); + $stmt->bindValue('q', $like); + $stmt->bindValue('lim', $size, \PDO::PARAM_INT); + $stmt->bindValue('off', $offset, \PDO::PARAM_INT); + $stmt->execute(); + $countStmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email LIKE :q'); + $countStmt->execute(['q' => $like]); + $count = (int)$countStmt->fetchColumn(); + } else { + $stmt = $pdo->prepare($baseSql . ' ORDER BY u.id DESC LIMIT :lim OFFSET :off'); + $stmt->bindValue('lim', $size, \PDO::PARAM_INT); + $stmt->bindValue('off', $offset, \PDO::PARAM_INT); + $stmt->execute(); + $count = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); + } + AuditService::log($adminId, 'users.list', 'users', ['page' => $page, 'size' => $size, 'q' => $q]); + Json::ok(['items' => $stmt->fetchAll(), 'total' => $count, 'page' => $page, 'size' => $size]); + } + + public function create(int $adminId): void + { + $body = Json::readBody(); + $email = trim((string)($body['email'] ?? '')); + $password = (string)($body['password'] ?? ''); + $role = (string)($body['role'] ?? 'user'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + Json::fail('bad_request', '邮箱格式不正确', 400); + } + if (strlen($password) < 8) { + Json::fail('bad_request', '密码至少 8 位', 400); + } + if (!in_array($role, ['user', 'admin'], true)) { + Json::fail('bad_request', 'role 不合法', 400); + } + if ($role === 'admin') { + AdminPermission::requireFull($adminId); + } + $adminLevel = null; + if ($role === 'admin') { + $adminLevel = (string)($body['admin_level'] ?? 'ops'); + if (!in_array($adminLevel, ['full', 'ops'], true)) { + Json::fail('bad_request', 'admin_level 不合法', 400); + } + } + $pdo = Db::pdo(); + $dup = $pdo->prepare('SELECT id FROM users WHERE email = :e'); + $dup->execute(['e' => $email]); + if ($dup->fetch()) { + Json::fail('conflict', '邮箱已存在', 409); + } + $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); + $stmt = $pdo->prepare( + 'INSERT INTO users (email, password_hash, role, admin_level, status, created_at) ' + . 'VALUES (:e, :h, :r, :lv, \'active\', :ts)' + ); + $stmt->execute([ + 'e' => $email, + 'h' => $hash, + 'r' => $role, + 'lv' => $adminLevel, + 'ts' => date('Y-m-d H:i:s'), + ]); + $id = (int)$pdo->lastInsertId(); + AuditService::log($adminId, 'users.create', 'user:' . $id, ['email' => $email, 'role' => $role]); + Json::ok(['id' => $id, 'email' => $email, 'role' => $role]); + } + + public function detail(int $adminId, int $id): void + { + $pdo = Db::pdo(); + $user = self::findUser($pdo, $id); + if ($user === null) { + Json::fail('not_found', '用户不存在', 404); + } + + $subStmt = $pdo->prepare( + 'SELECT s.id AS subscription_id, s.plan_id, s.status, s.started_at, s.expires_at, p.name AS plan_name, p.code AS plan_code ' + . 'FROM subscriptions s JOIN plans p ON p.id = s.plan_id ' + . 'WHERE s.user_id = :uid ORDER BY (s.status = \'active\') DESC, s.id DESC LIMIT 1' + ); + $subStmt->execute(['uid' => $id]); + $subscription = $subStmt->fetch() ?: null; + + $usageStmt = $pdo->prepare( + 'SELECT COUNT(*) AS files_count, COALESCE(SUM(size), 0) AS storage_bytes ' + . 'FROM soon_files WHERE user_id = :uid AND deleted_at IS NULL' + ); + $usageStmt->execute(['uid' => $id]); + $usage = $usageStmt->fetch() ?: ['files_count' => 0, 'storage_bytes' => 0]; + + $orderStmt = $pdo->prepare( + 'SELECT id, order_no, amount_cents, status, channel, created_at FROM pay_orders ' + . 'WHERE user_id = :uid ORDER BY id DESC LIMIT 5' + ); + $orderStmt->execute(['uid' => $id]); + $recentOrders = $orderStmt->fetchAll(); + + AuditService::log($adminId, 'users.detail', 'user:' . $id); + Json::ok([ + 'user' => $user, + 'subscription' => $subscription, + 'usage' => [ + 'files_count' => (int)$usage['files_count'], + 'storage_bytes' => (int)$usage['storage_bytes'], + ], + 'recent_orders' => $recentOrders, + ]); + } + + public function update(int $adminId, int $id): void + { + $pdo = Db::pdo(); + $user = self::findUser($pdo, $id); + if ($user === null) { + Json::fail('not_found', '用户不存在', 404); + } + $body = Json::readBody(); + $email = array_key_exists('email', $body) ? trim((string)$body['email']) : null; + $role = array_key_exists('role', $body) ? (string)$body['role'] : null; + $adminLevel = array_key_exists('admin_level', $body) ? (string)$body['admin_level'] : null; + + if ($email !== null) { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + Json::fail('bad_request', '邮箱格式不正确', 400); + } + $dup = $pdo->prepare('SELECT id FROM users WHERE email = :e AND id != :id'); + $dup->execute(['e' => $email, 'id' => $id]); + if ($dup->fetch()) { + Json::fail('conflict', '邮箱已被使用', 409); + } + } + if ($role !== null && !in_array($role, ['user', 'admin'], true)) { + Json::fail('bad_request', 'role 不合法', 400); + } + if ($role === 'user' && $user['role'] === 'admin') { + self::assertAdminRemovable($pdo, $id); + } + if ($role === 'admin' || $user['role'] === 'admin' || $adminLevel !== null) { + AdminPermission::requireFull($adminId); + } + if ($adminLevel !== null && !in_array($adminLevel, ['full', 'ops'], true)) { + Json::fail('bad_request', 'admin_level 不合法', 400); + } + if ($role !== null && $role !== 'admin' && $adminLevel !== null) { + Json::fail('bad_request', '仅管理员账号可设置 admin_level', 400); + } + + $sets = []; + $params = ['id' => $id]; + if ($email !== null) { + $sets[] = 'email = :e'; + $params['e'] = $email; + } + if ($role !== null) { + $sets[] = 'role = :r'; + $params['r'] = $role; + if ($role === 'user') { + $sets[] = 'admin_level = NULL'; + } + } + if ($adminLevel !== null) { + $sets[] = 'admin_level = :lv'; + $params['lv'] = $adminLevel; + } + if ($sets === []) { + Json::fail('bad_request', '无更新字段', 400); + } + $pdo->prepare('UPDATE users SET ' . implode(', ', $sets) . ' WHERE id = :id')->execute($params); + AuditService::log($adminId, 'users.update', 'user:' . $id, [ + 'email' => $email, + 'role' => $role, + 'admin_level' => $adminLevel, + ]); + Json::ok(['id' => $id]); + } + + public function delete(int $adminId, int $id): void + { + if ($id === $adminId) { + Json::fail('bad_request', '不能删除自己的账号', 400); + } + $pdo = Db::pdo(); + $user = self::findUser($pdo, $id); + if ($user === null) { + Json::fail('not_found', '用户不存在', 404); + } + if ($user['role'] === 'admin') { + AdminPermission::requireFull($adminId); + self::assertAdminRemovable($pdo, $id); + } + $pdo->beginTransaction(); + try { + $pdo->prepare('UPDATE soon_files SET deleted_at = :ts WHERE user_id = :u AND deleted_at IS NULL') + ->execute(['ts' => date('Y-m-d H:i:s'), 'u' => $id]); + $pdo->prepare('DELETE FROM subscriptions WHERE user_id = :u')->execute(['u' => $id]); + $pdo->prepare('DELETE FROM users WHERE id = :id')->execute(['id' => $id]); + $pdo->commit(); + } catch (\Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + AuditService::log($adminId, 'users.delete', 'user:' . $id, ['email' => $user['email']]); + Json::ok(['id' => $id]); + } + + public function setPassword(int $adminId, int $id): void + { + $pdo = Db::pdo(); + if (self::findUser($pdo, $id) === null) { + Json::fail('not_found', '用户不存在', 404); + } + $password = (string)(Json::readBody()['password'] ?? ''); + if (strlen($password) < 8) { + Json::fail('bad_request', '密码至少 8 位', 400); + } + $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); + $pdo->prepare('UPDATE users SET password_hash = :h WHERE id = :id')->execute(['h' => $hash, 'id' => $id]); + AuditService::log($adminId, 'users.setPassword', 'user:' . $id); + Json::ok(['id' => $id]); + } + + public function setSubscription(int $adminId, int $id): void + { + $pdo = Db::pdo(); + if (self::findUser($pdo, $id) === null) { + Json::fail('not_found', '用户不存在', 404); + } + $body = Json::readBody(); + $planId = (int)($body['plan_id'] ?? 0); + $days = (int)($body['days'] ?? 0); + if ($planId <= 0) { + Json::fail('bad_request', 'plan_id 必填', 400); + } + $planStmt = $pdo->prepare('SELECT * FROM plans WHERE id = :id'); + $planStmt->execute(['id' => $planId]); + $plan = $planStmt->fetch(); + if (!$plan) { + Json::fail('not_found', '套餐不存在', 404); + } + $durationDays = $days > 0 ? $days : (int)$plan['duration_days']; + $pdo->beginTransaction(); + try { + $pdo->prepare('UPDATE subscriptions SET status = \'expired\' WHERE user_id = :u AND status = \'active\'') + ->execute(['u' => $id]); + if ($plan['code'] !== 'free' && $durationDays > 0) { + $now = date('Y-m-d H:i:s'); + $expires = date('Y-m-d H:i:s', time() + $durationDays * 86400); + $pdo->prepare( + 'INSERT INTO subscriptions (user_id, plan_id, status, started_at, expires_at) ' + . 'VALUES (:u, :p, \'active\', :sa, :ea)' + )->execute(['u' => $id, 'p' => $planId, 'sa' => $now, 'ea' => $expires]); + } + $pdo->commit(); + } catch (\Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + AuditService::log($adminId, 'users.setSubscription', 'user:' . $id, [ + 'plan_id' => $planId, + 'days' => $durationDays, + ]); + Json::ok(['id' => $id, 'plan_id' => $planId]); + } + + public function setStatus(int $adminId, int $id): void + { + $body = Json::readBody(); + $status = (string)($body['status'] ?? ''); + if (!in_array($status, ['active', 'disabled'], true)) { + Json::fail('bad_request', 'status 必须为 active 或 disabled', 400); + } + if ($id === $adminId && $status === 'disabled') { + Json::fail('bad_request', '不能停用自己的账号', 400); + } + $pdo = Db::pdo(); + $user = self::findUser($pdo, $id); + if ($user === null) { + Json::fail('not_found', '用户不存在', 404); + } + if ($status === 'disabled' && $user['role'] === 'admin') { + self::assertAdminRemovable($pdo, $id); + } + $pdo->prepare('UPDATE users SET status = :s WHERE id = :id')->execute(['s' => $status, 'id' => $id]); + AuditService::log($adminId, 'users.setStatus', 'user:' . $id, ['status' => $status]); + Json::ok(['id' => $id, 'status' => $status]); + } + + /** @return array|null */ + private static function findUser(\PDO $pdo, int $id): ?array + { + $stmt = $pdo->prepare('SELECT id, email, role, admin_level, status, created_at FROM users WHERE id = :id'); + $stmt->execute(['id' => $id]); + $row = $stmt->fetch(); + return $row ?: null; + } + + private static function assertAdminRemovable(\PDO $pdo, int $excludeId): void + { + $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE role = \'admin\' AND status = \'active\' AND id != :id'); + $stmt->execute(['id' => $excludeId]); + if ((int)$stmt->fetchColumn() < 1) { + Json::fail('bad_request', '至少保留一名活跃管理员', 400); + } + } +} diff --git a/backend-web/src/Admin/Services/PaymentKeyStore.php b/backend-web/src/Admin/Services/PaymentKeyStore.php new file mode 100644 index 0000000..91b80e2 --- /dev/null +++ b/backend-web/src/Admin/Services/PaymentKeyStore.php @@ -0,0 +1,65 @@ + ['private_key', 'public_key'], + 'wechat' => ['mch_private_key', 'api_v3_key', 'platform_cert'], + ]; + if (!isset($allowed[$channel]) || !in_array($kind, $allowed[$channel], true)) { + Json::fail('bad_request', 'kind 不允许', 400); + } + $path = self::dir() . '/' . $channel . '_' . $kind . '.pem'; + @file_put_contents($path, $content); + @chmod($path, 0600); + AuditService::log($adminId, 'payment.key.upload', $channel . ':' . $kind, [ + 'bytes' => strlen($content), + ]); + $config = Config::all(); + PaymentConfigLoader::merge($config); + Config::init($config); + return ['channel' => $channel, 'kind' => $kind, 'bytes' => strlen($content)]; + } + + public static function status(int $adminId): array + { + $dir = self::dir(); + $items = []; + foreach (glob($dir . '/*.pem') ?: [] as $f) { + $items[] = [ + 'name' => basename($f), + 'bytes' => filesize($f), + 'mtime' => date('Y-m-d H:i:s', (int)filemtime($f)), + ]; + } + AuditService::log($adminId, 'payment.key.status', 'payment'); + return ['items' => $items]; + } +} diff --git a/backend-web/src/Controllers/AuthController.php b/backend-web/src/Controllers/AuthController.php new file mode 100644 index 0000000..629217a --- /dev/null +++ b/backend-web/src/Controllers/AuthController.php @@ -0,0 +1,44 @@ + 0 ? $size : max(1, min(200, (int)($_GET['limit'] ?? 50))); + $offset = $page > 0 ? ($page - 1) * $limit : max(0, (int)($_GET['offset'] ?? 0)); + if ($page > 0) { + $list = FileService::list($u['id'], $limit, $offset); + Json::ok([ + 'items' => $list, + 'total' => FileService::fileCount($u['id']), + 'page' => $page, + 'size' => $limit, + ]); + return; + } + $list = FileService::list($u['id'], $limit, $offset); + Json::ok([ + 'items' => $list, + 'total' => FileService::fileCount($u['id']), + 'limit' => $limit, + 'offset' => $offset, + ]); + } + + public function create(): void + { + $u = Auth::require(); + MembershipService::requireActiveMember($u['id']); + $body = Json::readBody(); + $name = (string)($body['name'] ?? 'untitled.soon'); + $json = (string)($body['json'] ?? '{}'); + Json::ok(FileService::create($u['id'], $name, $json)); + } + + public function update(int $id): void + { + $u = Auth::require(); + MembershipService::requireActiveMember($u['id']); + $body = Json::readBody(); + $name = (string)($body['name'] ?? 'untitled.soon'); + $json = (string)($body['json'] ?? '{}'); + $version = isset($body['version']) ? (int)$body['version'] : null; + Json::ok(FileService::update($u['id'], $id, $name, $json, $version)); + } + + public function delete(int $id): void + { + $u = Auth::require(); + FileService::softDelete($u['id'], $id); + Json::ok(['id' => $id]); + } + + public function download(int $id): void + { + $u = Auth::require(); + $accept = (string)($_SERVER['HTTP_ACCEPT'] ?? ''); + $jsonRead = stripos($accept, 'application/json') !== false + && stripos($accept, 'application/octet-stream') === false; + if (!$jsonRead) { + MembershipService::requireActiveMember($u['id']); + } + $row = FileService::fetch($u['id'], $id); + $name = (string)$row['name']; + $json = (string)$row['json']; + $size = strlen($json); + $etag = '"' . md5($json) . '"'; + if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) { + http_response_code(304); + exit; + } + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . rawurlencode($name) . '"'); + header('Content-Length: ' . $size); + header('ETag: ' . $etag); + header('Accept-Ranges: bytes'); + header('Cache-Control: private, max-age=0, must-revalidate'); + $start = 0; + $end = $size - 1; + if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) { + $start = (int)$m[1]; + if ($m[2] !== '') $end = (int)$m[2]; + if ($end >= $size) $end = $size - 1; + http_response_code(206); + header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size); + header('Content-Length: ' . ($end - $start + 1)); + } + $out = fopen('php://output', 'wb'); + fwrite($out, substr($json, $start, $end - $start + 1)); + fclose($out); + exit; + } +} diff --git a/backend-web/src/Controllers/PayController.php b/backend-web/src/Controllers/PayController.php new file mode 100644 index 0000000..be795d5 --- /dev/null +++ b/backend-web/src/Controllers/PayController.php @@ -0,0 +1,156 @@ + $orderNo, 'status' => 'cancelled']); + } + + public function showOrder(string $orderNo): void + { + $u = Auth::require(); + $order = PayService::findByOrderNoForUser($orderNo, $u['id']); + if ($order === null) { + Json::fail('not_found', '订单不存在', 404); + } + Json::ok([ + 'order_no' => $order['order_no'], + 'status' => $order['status'], + 'amount_cents' => (int)$order['amount_cents'], + 'channel' => $order['channel'], + 'paid_at' => $order['paid_at'], + 'refund_status' => $order['refund_status'] ?? 'none', + 'refund_reason' => $order['refund_reason'] ?? null, + ]); + } + + public function requestRefund(string $orderNo): void + { + $u = Auth::require(); + $body = Json::readBody(); + $reason = trim((string)($body['reason'] ?? '')); + PayService::requestRefund($u['id'], $orderNo, $reason); + Json::ok(['order_no' => $orderNo, 'refund_status' => 'pending']); + } + + public function alipayNotify(): void + { + $raw = file_get_contents('php://input') ?: ''; + parse_str($raw, $params); + if (empty($params) && !empty($_POST)) { + $params = $_POST; + } + if (!AlipayClient::verifyNotify($params)) { + http_response_code(400); + echo 'fail'; + exit; + } + $status = (string)($params['trade_status'] ?? ''); + if (!in_array($status, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) { + echo 'success'; + exit; + } + $orderNo = (string)($params['out_trade_no'] ?? ''); + $tradeNo = (string)($params['trade_no'] ?? ''); + $amountCents = (int)round((float)($params['total_amount'] ?? 0) * 100); + if (PayService::markPaid($orderNo, 'alipay', $tradeNo, $amountCents)) { + echo 'success'; + } else { + echo 'fail'; + } + exit; + } + + public function wechatNotify(): void + { + $body = file_get_contents('php://input') ?: ''; + $signature = $_SERVER['HTTP_WEIXINPAY_SIGNATURE'] ?? $_SERVER['HTTP_WEIXINPAY2_SIGNATURE'] ?? ''; + $serial = $_SERVER['HTTP_WEIXINPAY_SERIAL'] ?? $_SERVER['HTTP_WEIXINPAY2_SERIAL'] ?? ''; + $timestamp = $_SERVER['HTTP_WEIXINPAY_TIMESTAMP'] ?? $_SERVER['HTTP_WEIXINPAY2_TIMESTAMP'] ?? ''; + $nonce = $_SERVER['HTTP_WEIXINPAY_NONCE'] ?? $_SERVER['HTTP_WEIXINPAY2_NONCE'] ?? ''; + if (!WeChatClient::verifyNotify($body, $signature, $serial, $timestamp, $nonce)) { + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode(['code' => 'FAIL', 'message' => '验签失败']); + exit; + } + $data = json_decode($body, true); + $plain = WeChatClient::decryptResource( + (string)($data['resource']['ciphertext'] ?? ''), + (string)($data['resource']['associated_data'] ?? ''), + (string)($data['resource']['nonce'] ?? ''), + (string)\Soon\Api\Core\Config::get('wechat.api_v3_key', '') + ); + if ($plain === null) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(['code' => 'FAIL', 'message' => '解密失败']); + exit; + } + $decoded = json_decode($plain, true); + $orderNo = (string)($decoded['out_trade_no'] ?? ''); + $txnId = (string)($decoded['transaction_id'] ?? ''); + $amountCents = (int)($decoded['amount']['total'] ?? 0); + $tradeState = (string)($decoded['trade_state'] ?? ''); + if ($tradeState !== '' && $tradeState !== 'SUCCESS') { + header('Content-Type: application/json'); + echo json_encode(['code' => 'SUCCESS']); + exit; + } + if (!PayService::markPaid($orderNo, 'wechat', $txnId, $amountCents)) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(['code' => 'FAIL', 'message' => '订单处理失败']); + exit; + } + header('Content-Type: application/json'); + echo json_encode(['code' => 'SUCCESS']); + exit; + } +} diff --git a/backend-web/src/Controllers/PlanController.php b/backend-web/src/Controllers/PlanController.php new file mode 100644 index 0000000..bb02f2d --- /dev/null +++ b/backend-web/src/Controllers/PlanController.php @@ -0,0 +1,25 @@ + MembershipService::plans()]); + } + + public function myPlan(): void + { + $u = Auth::require(); + Json::ok([ + 'membership' => MembershipService::currentPlan($u['id']), + 'recent_orders' => MembershipService::recentOrders($u['id']), + ]); + } +} diff --git a/backend-web/src/Controllers/SettingsController.php b/backend-web/src/Controllers/SettingsController.php new file mode 100644 index 0000000..3f41e02 --- /dev/null +++ b/backend-web/src/Controllers/SettingsController.php @@ -0,0 +1,15 @@ + [], 'count' => 0]); + } + $data = json_decode((string)file_get_contents($manifest), true); + if (!is_array($data)) { + Json::fail('manifest_invalid', '模型清单已损坏', 500); + } + Json::ok($data); + } +} diff --git a/backend-web/src/Core/Config.php b/backend-web/src/Core/Config.php new file mode 100644 index 0000000..bb9a5c6 --- /dev/null +++ b/backend-web/src/Core/Config.php @@ -0,0 +1,48 @@ +beginTransaction(); + try { + $result = $fn($pdo); + $pdo->commit(); + return $result; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } +} diff --git a/backend-web/src/Core/Json.php b/backend-web/src/Core/Json.php new file mode 100644 index 0000000..c091cfc --- /dev/null +++ b/backend-web/src/Core/Json.php @@ -0,0 +1,41 @@ + true, 'data' => $data], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + public static function fail(string $code, string $message, int $status = 400, array $extra = []): void + { + http_response_code($status); + header('Content-Type: application/json; charset=utf-8'); + $payload = array_merge(['ok' => false, 'error' => $code, 'message' => $message], $extra); + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + public static function readBody(): array + { + $raw = file_get_contents('php://input') ?: ''; + if ($raw === '') { + return []; + } + $data = json_decode($raw, true); + if (!is_array($data)) { + self::fail('bad_request', '请求体不是合法 JSON', 400); + } + return $data; + } +} diff --git a/backend-web/src/Core/Jwt.php b/backend-web/src/Core/Jwt.php new file mode 100644 index 0000000..fe3e562 --- /dev/null +++ b/backend-web/src/Core/Jwt.php @@ -0,0 +1,83 @@ + 'HS256', 'typ' => 'JWT']; + $payload = array_merge([ + 'iss' => self::issuer(), + 'iat' => time(), + 'exp' => time() + self::ttl(), + ], $claims); + $h = self::b64u(json_encode($header, JSON_UNESCAPED_UNICODE)); + $p = self::b64u(json_encode($payload, JSON_UNESCAPED_UNICODE)); + $sig = hash_hmac('sha256', $h . '.' . $p, self::secret(), true); + return $h . '.' . $p . '.' . self::b64u($sig); + } + + public static function decode(string $token): ?array + { + $parts = explode('.', $token); + if (count($parts) !== 3) { + return null; + } + [$h, $p, $s] = $parts; + $expected = self::b64u(hash_hmac('sha256', $h . '.' . $p, self::secret(), true)); + if (!hash_equals($expected, $s)) { + return null; + } + $payload = json_decode(self::b64uDecode($p), true); + if (!is_array($payload)) { + return null; + } + if (!isset($payload['iss']) || $payload['iss'] !== self::issuer()) { + return null; + } + if (isset($payload['exp']) && $payload['exp'] < time()) { + return null; + } + return $payload; + } + + private static function b64u(string $bin): string + { + return rtrim(strtr(base64_encode($bin), '+/', '-_'), '='); + } + + private static function b64uDecode(string $b64): string + { + $b64 = strtr($b64, '-_', '+/'); + $pad = 4 - (strlen($b64) % 4); + if ($pad < 4) { + $b64 .= str_repeat('=', $pad); + } + return base64_decode($b64); + } +} diff --git a/backend-web/src/Core/Logger.php b/backend-web/src/Core/Logger.php new file mode 100644 index 0000000..99ca521 --- /dev/null +++ b/backend-web/src/Core/Logger.php @@ -0,0 +1,31 @@ +> */ + private static array $routes = []; + + public static function get(string $path, callable|array $handler): void + { + self::add('GET', $path, $handler); + } + + public static function post(string $path, callable|array $handler): void + { + self::add('POST', $path, $handler); + } + + public static function put(string $path, callable|array $handler): void + { + self::add('PUT', $path, $handler); + } + + public static function delete(string $path, callable|array $handler): void + { + self::add('DELETE', $path, $handler); + } + + public static function add(string $method, string $path, callable|array $handler): void + { + self::$routes[$method][$path] = $handler; + } + + /** + * @return array{0: callable|array, 1: array}|null + */ + public static function dispatch(string $method, string $path): ?array + { + $method = strtoupper($method); + $candidates = self::$routes[$method] ?? []; + foreach ($candidates as $pattern => $handler) { + $regex = self::compile($pattern); + if (preg_match($regex, $path, $m)) { + $params = []; + foreach ($m as $k => $v) { + if (!is_int($k)) { + $params[$k] = $v; + } + } + return [$handler, $params]; + } + } + return null; + } + + public static function compile(string $pattern): string + { + $regex = preg_replace_callback('#\{([a-zA-Z_][a-zA-Z0-9_]*)\}#', static function ($m) { + return '(?P<' . $m[1] . '>[^/]+)'; + }, $pattern); + return '#^' . $regex . '$#'; + } +} diff --git a/backend-web/src/Middleware/AdminAuth.php b/backend-web/src/Middleware/AdminAuth.php new file mode 100644 index 0000000..0dbca10 --- /dev/null +++ b/backend-web/src/Middleware/AdminAuth.php @@ -0,0 +1,40 @@ +prepare('SELECT role, status FROM users WHERE id = :id'); + $stmt->execute(['id' => $uid]); + $u = $stmt->fetch(); + if (!$u || $u['status'] === 'disabled' || $u['role'] !== 'admin') { + Json::fail('forbidden', '需要管理员权限', 403); + } + return $uid; + } +} diff --git a/backend-web/src/Middleware/Auth.php b/backend-web/src/Middleware/Auth.php new file mode 100644 index 0000000..bd243cb --- /dev/null +++ b/backend-web/src/Middleware/Auth.php @@ -0,0 +1,62 @@ +prepare('SELECT id, email, role, admin_level, status FROM users WHERE id = :id'); + $stmt->execute(['id' => $uid]); + $user = $stmt->fetch(); + if (!$user || $user['status'] === 'disabled') { + Json::fail('unauthorized', '账号不存在或已停用', 401); + } + $out = [ + 'id' => (int)$user['id'], + 'email' => $user['email'], + 'role' => $user['role'], + ]; + if ($user['role'] === 'admin') { + $level = (string)($user['admin_level'] ?? 'full'); + $out['admin_level'] = $level === 'ops' ? 'ops' : 'full'; + } + return $out; + } +} diff --git a/backend-web/src/Middleware/RateLimit.php b/backend-web/src/Middleware/RateLimit.php new file mode 100644 index 0000000..5ed9a4d --- /dev/null +++ b/backend-web/src/Middleware/RateLimit.php @@ -0,0 +1,84 @@ + ['capacity' => 60, 'window' => 60], 'POST /api/v1/auth/login' => [...]] + */ +final class RateLimit +{ + public static function dir(): string + { + $dir = SOON_SERVER_ROOT . '/storage/cache/rate'; + if (!is_dir($dir)) { + @mkdir($dir, 0775, true); + } + return $dir; + } + + public static function keyFor(string $method, string $path, ?int $userId): string + { + if ($userId !== null) { + return 'u' . $userId; + } + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + return 'ip' . md5($ip); + } + + public static function matchRule(string $method, string $path, array $rules): ?array + { + $needle = $method . ' ' . $path; + foreach ($rules as $pattern => $rule) { + if ($pattern === 'admin/*' && str_starts_with($path, '/api/admin/')) { + return $rule; + } + if (str_contains($pattern, ' ')) { + if ($pattern === $needle) { + return $rule; + } + continue; + } + $regex = '#^' . str_replace('\*', '.*', preg_quote($pattern, '#')) . '$#'; + if (preg_match($regex, $path)) { + return $rule; + } + } + return null; + } + + public static function check(string $routeKey, string $method, string $path, ?int $userId): void + { + $rules = Config::get('rate_limits', []); + $rule = self::matchRule($method, $path, $rules); + if ($rule === null && $routeKey === 'admin/*') { + $rule = $rules['admin/*'] ?? null; + } + if ($rule === null) { + return; + } + $capacity = (int)($rule['capacity'] ?? 60); + $window = (int)($rule['window'] ?? 60); + $bucket = self::dir() . '/' . md5($routeKey . ':' . self::keyFor($method, $path, $userId)) . '.json'; + $now = time(); + $data = ['start' => $now, 'count' => 0]; + if (is_file($bucket)) { + $raw = json_decode((string)file_get_contents($bucket), true); + if (is_array($raw) && isset($raw['start'], $raw['count'])) { + if ($now - (int)$raw['start'] < $window) { + $data = $raw; + } + } + } + $data['count']++; + @file_put_contents($bucket, json_encode($data)); + if ($data['count'] > $capacity) { + Json::fail('rate_limited', '请求过于频繁,请稍后再试', 429); + } + } +} diff --git a/backend-web/src/Services/AdminPermission.php b/backend-web/src/Services/AdminPermission.php new file mode 100644 index 0000000..0b99dfe --- /dev/null +++ b/backend-web/src/Services/AdminPermission.php @@ -0,0 +1,44 @@ +prepare( + 'SELECT admin_level FROM users WHERE id = :id AND role = \'admin\' AND status = \'active\'' + ); + $stmt->execute(['id' => $adminId]); + $row = $stmt->fetch(); + if (!$row) { + return 'full'; + } + $level = (string)($row['admin_level'] ?? 'full'); + return $level === 'ops' ? 'ops' : 'full'; + } + + public static function requireFull(int $adminId): void + { + if (self::level($adminId) !== 'full') { + Json::fail('forbidden', '需要超级管理员权限', 403); + } + } + + public static function assertPath(int $adminId, string $path): void + { + if (self::level($adminId) === 'full') { + return; + } + $prefixes = ['/api/admin/settings', '/api/admin/audits', '/api/admin/payment']; + foreach ($prefixes as $prefix) { + if (str_starts_with($path, $prefix)) { + Json::fail('forbidden', '需要超级管理员权限', 403); + } + } + } +} diff --git a/backend-web/src/Services/AlipayClient.php b/backend-web/src/Services/AlipayClient.php new file mode 100644 index 0000000..24c9b90 --- /dev/null +++ b/backend-web/src/Services/AlipayClient.php @@ -0,0 +1,154 @@ +} */ + public static function pagePayForm(string $orderNo, int $amountCents, string $subject): array + { + $cfg = Config::get('alipay', []); + $params = [ + 'app_id' => (string)($cfg['app_id'] ?? ''), + 'method' => 'alipay.trade.page.pay', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'notify_url' => (string)Config::get('site.base_url', '') . '/api/v1/pay/alipay/notify', + 'return_url' => (string)Config::get('site.front_base_url', Config::get('site.base_url', '')) + . '/pages/member.web.html?paid=' . rawurlencode($orderNo), + 'biz_content' => json_encode([ + 'out_trade_no' => $orderNo, + 'product_code' => 'FAST_INSTANT_TRADE_PAY', + 'total_amount' => number_format($amountCents / 100, 2, '.', ''), + 'subject' => $subject, + ], JSON_UNESCAPED_UNICODE), + ]; + $params['sign'] = self::sign($params, (string)($cfg['private_key'] ?? '')); + $gateway = ($cfg['sandbox'] ?? false) + ? 'https://openapi.alipaydev.com/gateway.do' + : 'https://openapi.alipay.com/gateway.do'; + return ['gateway' => $gateway, 'params' => $params]; + } + + public static function pagePay(string $orderNo, int $amountCents, string $subject): string + { + $form = self::pagePayForm($orderNo, $amountCents, $subject); + return $form['gateway'] . '?' . http_build_query($form['params']); + } + + public static function verifyNotify(array $params): bool + { + $cfg = Config::get('alipay', []); + $sign = (string)($params['sign'] ?? ''); + if ($sign === '') { + return false; + } + unset($params['sign'], $params['sign_type']); + ksort($params); + $query = ''; + foreach ($params as $k => $v) { + if ($v === '' || $v === null) { + continue; + } + $query .= $k . '=' . $v . '&'; + } + $query = rtrim($query, '&'); + return self::verifyNotifySign($query, $sign, (string)($cfg['public_key'] ?? '')); + } + + public static function verifyNotifySign(string $content, string $signBase64, string $alipayPublicKey): bool + { + $pub = openssl_get_publickey(self::normalizePem($alipayPublicKey, 'PUBLIC KEY')); + if ($pub === false) { + return false; + } + $sig = base64_decode($signBase64, true); + if ($sig === false) { + openssl_free_key($pub); + return false; + } + $ok = openssl_verify($content, $sig, $pub, OPENSSL_ALGO_SHA256); + openssl_free_key($pub); + return $ok === 1; + } + + private static function sign(array $params, string $privateKey): string + { + ksort($params); + $query = ''; + foreach ($params as $k => $v) { + if ($v === '' || $v === null) { + continue; + } + $query .= $k . '=' . $v . '&'; + } + $query = rtrim($query, '&'); + $res = openssl_get_privatekey(self::normalizePem($privateKey, 'PRIVATE KEY')); + if ($res === false) { + return ''; + } + $signature = ''; + openssl_sign($query, $signature, $res, OPENSSL_ALGO_SHA256); + openssl_free_key($res); + return base64_encode($signature); + } + + public static function refund(string $orderNo, int $amountCents): bool + { + $cfg = Config::get('alipay', []); + $appId = (string)($cfg['app_id'] ?? ''); + $privateKey = (string)($cfg['private_key'] ?? ''); + if ($appId === '' || $privateKey === '') { + return false; + } + $params = [ + 'app_id' => $appId, + 'method' => 'alipay.trade.refund', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'biz_content' => json_encode([ + 'out_trade_no' => $orderNo, + 'refund_amount' => number_format($amountCents / 100, 2, '.', ''), + ], JSON_UNESCAPED_UNICODE), + ]; + $params['sign'] = self::sign($params, $privateKey); + $gateway = ($cfg['sandbox'] ?? false) + ? 'https://openapi.alipaydev.com/gateway.do' + : 'https://openapi.alipay.com/gateway.do'; + $ch = curl_init($gateway); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($params), + CURLOPT_TIMEOUT => 20, + ]); + $resp = curl_exec($ch); + curl_close($ch); + if (!is_string($resp) || $resp === '') { + return false; + } + $data = json_decode($resp, true); + $key = 'alipay_trade_refund_response'; + return is_array($data) && ($data[$key]['code'] ?? '') === '10000'; + } + + private static function normalizePem(string $key, string $type): string + { + $key = trim($key); + if (str_contains($key, 'BEGIN')) { + return $key; + } + $body = chunk_split(preg_replace('/\s+/', '', $key), 64, "\n"); + return "-----BEGIN {$type}-----\n{$body}-----END {$type}-----\n"; + } +} diff --git a/backend-web/src/Services/AuditService.php b/backend-web/src/Services/AuditService.php new file mode 100644 index 0000000..fc1259d --- /dev/null +++ b/backend-web/src/Services/AuditService.php @@ -0,0 +1,28 @@ +prepare( + 'INSERT INTO audit_logs (admin_id, action, target, context, ip, created_at) ' + . 'VALUES (:aid, :act, :tgt, :ctx, :ip, :ts)' + ); + $stmt->execute([ + 'aid' => $adminId, + 'act' => $action, + 'tgt' => $target, + 'ctx' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', + 'ts' => date('Y-m-d H:i:s'), + ]); + } +} diff --git a/backend-web/src/Services/AuthService.php b/backend-web/src/Services/AuthService.php new file mode 100644 index 0000000..6ff42c3 --- /dev/null +++ b/backend-web/src/Services/AuthService.php @@ -0,0 +1,100 @@ +prepare('SELECT id FROM users WHERE email = :e'); + $stmt->execute(['e' => $email]); + if ($stmt->fetch()) { + Json::fail('conflict', '邮箱已注册', 409); + } + $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); + $stmt = $pdo->prepare( + 'INSERT INTO users (email, password_hash, role, status, created_at) ' + . 'VALUES (:e, :h, "user", "active", :ts)' + ); + $stmt->execute([ + 'e' => $email, + 'h' => $hash, + 'ts' => date('Y-m-d H:i:s'), + ]); + $uid = (int)$pdo->lastInsertId(); + return self::issueTokens($uid); + } + + public static function login(string $email, string $password): array + { + $pdo = Db::pdo(); + $stmt = $pdo->prepare('SELECT id, password_hash, role, status FROM users WHERE email = :e'); + $stmt->execute(['e' => $email]); + $u = $stmt->fetch(); + if (!$u || !password_verify($password, $u['password_hash'])) { + Json::fail('unauthorized', '邮箱或密码错误', 401); + } + if ($u['status'] === 'disabled') { + Json::fail('forbidden', '账号已停用', 403); + } + return self::issueTokens((int)$u['id'], $u['role']); + } + + public static function refresh(string $refreshToken): array + { + $payload = Jwt::decode($refreshToken); + if ($payload === null || ($payload['typ'] ?? '') !== 'refresh') { + Json::fail('unauthorized', '刷新令牌无效', 401); + } + $uid = (int)($payload['sub'] ?? 0); + if ($uid <= 0) { + Json::fail('unauthorized', '刷新令牌主体错误', 401); + } + $stmt = Db::pdo()->prepare('SELECT role, status FROM users WHERE id = :id'); + $stmt->execute(['id' => $uid]); + $u = $stmt->fetch(); + if (!$u || $u['status'] === 'disabled') { + Json::fail('unauthorized', '账号不存在', 401); + } + return self::issueTokens($uid, $u['role']); + } + + public static function userFromAccessToken(): array + { + $u = Auth::require(); + return $u; + } + + private static function issueTokens(int $uid, string $role = 'user'): array + { + $access = Jwt::encode(['sub' => $uid, 'role' => $role, 'typ' => 'access']); + $refresh = Jwt::encode([ + 'sub' => $uid, + 'role' => $role, + 'typ' => 'refresh', + 'exp' => time() + Jwt::refreshTtl(), + ]); + return [ + 'access_token' => $access, + 'refresh_token' => $refresh, + 'token_type' => 'Bearer', + 'expires_in' => Jwt::ttl(), + ]; + } +} diff --git a/backend-web/src/Services/FileService.php b/backend-web/src/Services/FileService.php new file mode 100644 index 0000000..e457172 --- /dev/null +++ b/backend-web/src/Services/FileService.php @@ -0,0 +1,155 @@ +prepare('SELECT COALESCE(SUM(size),0) AS s FROM soon_files WHERE user_id = :u AND deleted_at IS NULL'); + $stmt->execute(['u' => $userId]); + return (int)$stmt->fetchColumn(); + } + + public static function fileCount(int $userId): int + { + $stmt = Db::pdo()->prepare('SELECT COUNT(*) FROM soon_files WHERE user_id = :u AND deleted_at IS NULL'); + $stmt->execute(['u' => $userId]); + return (int)$stmt->fetchColumn(); + } + + public static function list(int $userId, int $limit, int $offset): array + { + $stmt = Db::pdo()->prepare( + 'SELECT id, name, size, version, updated_at, created_at ' + . 'FROM soon_files WHERE user_id = :u AND deleted_at IS NULL ' + . 'ORDER BY updated_at DESC LIMIT :lim OFFSET :off' + ); + $stmt->bindValue('u', $userId, \PDO::PARAM_INT); + $stmt->bindValue('lim', $limit, \PDO::PARAM_INT); + $stmt->bindValue('off', $offset, \PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(); + } + + public static function create(int $userId, string $name, string $json): array + { + $maxFiles = MembershipService::maxFilesLimit($userId); + if (self::fileCount($userId) >= $maxFiles) { + Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413); + } + $size = strlen($json); + $quota = self::quotaBytes($userId); + $used = self::usedBytes($userId); + if ($quota > 0 && $used + $size > $quota) { + Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413); + } + $now = date('Y-m-d H:i:s'); + $stmt = Db::pdo()->prepare( + 'INSERT INTO soon_files (user_id, name, json, size, version, created_at, updated_at) ' + . 'VALUES (:u, :n, :j, :s, 1, :created_at, :updated_at)' + ); + $stmt->execute(['u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'created_at' => $now, 'updated_at' => $now]); + $id = (int)Db::pdo()->lastInsertId(); + return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now]; + } + + public static function update(int $userId, int $id, string $name, string $json, ?int $expectedVersion): array + { + $newSize = strlen($json); + $quota = self::quotaBytes($userId); + $used = self::usedBytes($userId); + $pdo = Db::pdo(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('SELECT * FROM soon_files WHERE id = :id AND user_id = :u AND deleted_at IS NULL FOR UPDATE'); + $stmt->execute(['id' => $id, 'u' => $userId]); + $row = $stmt->fetch(); + if (!$row) { + throw new \RuntimeException('not_found'); + } + if ($expectedVersion !== null && (int)$row['version'] !== $expectedVersion) { + throw new \RuntimeException('version_conflict'); + } + $oldSize = (int)$row['size']; + if ($quota > 0 && ($used - $oldSize + $newSize) > $quota) { + throw new \RuntimeException('quota_exceeded'); + } + $now = date('Y-m-d H:i:s'); + $newVersion = (int)$row['version'] + 1; + $upd = $pdo->prepare( + 'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, updated_at = :ts ' + . 'WHERE id = :id AND version = :cv' + ); + $upd->execute([ + 'n' => $name, 'j' => $json, 's' => $newSize, 'v' => $newVersion, + 'ts' => $now, 'id' => $id, 'cv' => (int)$row['version'], + ]); + if ($upd->rowCount() === 0) { + throw new \RuntimeException('version_conflict'); + } + $pdo->commit(); + return ['id' => $id, 'name' => $name, 'size' => $newSize, 'version' => $newVersion, 'updated_at' => $now]; + } catch (\RuntimeException $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + if ($e->getMessage() === 'not_found') Json::fail('not_found', '文件不存在', 404); + if ($e->getMessage() === 'version_conflict') Json::fail('conflict', '版本冲突,请刷新后重试', 409); + if ($e->getMessage() === 'quota_exceeded') Json::fail('quota_exceeded', '存储空间已满', 413); + throw $e; + } catch (\Throwable $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + throw $e; + } + } + + public static function softDelete(int $userId, int $id): void + { + $stmt = Db::pdo()->prepare('UPDATE soon_files SET deleted_at = :ts WHERE id = :id AND user_id = :u'); + $stmt->execute(['ts' => date('Y-m-d H:i:s'), 'id' => $id, 'u' => $userId]); + } + + public static function fetch(int $userId, int $id): array + { + $stmt = Db::pdo()->prepare('SELECT * FROM soon_files WHERE id = :id AND user_id = :u AND deleted_at IS NULL'); + $stmt->execute(['id' => $id, 'u' => $userId]); + $row = $stmt->fetch(); + if (!$row) { + Json::fail('not_found', '文件不存在', 404); + } + return $row; + } +} diff --git a/backend-web/src/Services/MembershipService.php b/backend-web/src/Services/MembershipService.php new file mode 100644 index 0000000..402a7cd --- /dev/null +++ b/backend-web/src/Services/MembershipService.php @@ -0,0 +1,406 @@ +query( + 'SELECT id, code, name, description, price_cents, quota_mb, max_files, duration_days, ' + . 'features, sort_order, is_recommended, is_active FROM plans WHERE is_active = 1 ' + . 'ORDER BY sort_order ASC, price_cents ASC' + ); + $items = []; + foreach ($stmt->fetchAll() as $row) { + $items[] = self::enrichPlan($row); + } + return $items; + } + + public static function currentPlan(int $userId): array + { + $pdo = Db::pdo(); + $usage = self::usageStats($userId); + + $stmt = $pdo->prepare( + 'SELECT p.*, s.id AS subscription_id, s.expires_at AS subscription_expires_at, s.started_at AS subscription_started_at ' + . 'FROM subscriptions s JOIN plans p ON p.id = s.plan_id ' + . 'WHERE s.user_id = :u AND s.status = "active" AND s.expires_at > NOW() ' + . 'ORDER BY s.expires_at DESC LIMIT 1' + ); + $stmt->execute(['u' => $userId]); + $row = $stmt->fetch(); + + if ($row) { + $plan = self::enrichPlan($row); + $expiresAt = (string)($row['subscription_expires_at'] ?? ''); + $daysRemaining = self::daysUntil($expiresAt); + $plan['subscription'] = [ + 'id' => (int)$row['subscription_id'], + 'started_at' => $row['subscription_started_at'] ?? null, + 'expires_at' => $expiresAt, + 'days_remaining' => $daysRemaining, + 'status' => $daysRemaining <= 7 ? 'expiring' : 'active', + ]; + $plan['usage'] = $usage; + $plan['tier'] = 'member'; + $plan['is_member'] = true; + return $plan; + } + + $memberQuota = (int)Config::get('limits.member_quota_mb', 2048); + $memberFiles = (int)Config::get('limits.member_max_files', 200); + $freeQuota = (int)Config::get('limits.free_quota_mb', $memberQuota); + $freeStmt = $pdo->prepare('SELECT * FROM plans WHERE code = "free" AND is_active = 1 LIMIT 1'); + $freeStmt->execute(); + $freeRow = $freeStmt->fetch(); + if ($freeRow) { + $plan = self::enrichPlan($freeRow); + } else { + $plan = self::enrichPlan([ + 'id' => 0, + 'code' => 'free', + 'name' => '免费版', + 'description' => '适合个人体验与轻量设计', + 'price_cents' => 0, + 'quota_mb' => $freeQuota, + 'max_files' => $memberFiles, + 'duration_days' => 0, + 'features' => '[]', + 'sort_order' => 0, + 'is_recommended' => 0, + 'is_active' => 1, + ]); + } + $plan['subscription'] = [ + 'id' => null, + 'started_at' => null, + 'expires_at' => null, + 'days_remaining' => null, + 'status' => 'free', + ]; + $plan['usage'] = $usage; + $plan['tier'] = 'free'; + $plan['is_member'] = false; + return $plan; + } + + public static function isActiveMember(int $userId): bool + { + $stmt = Db::pdo()->prepare( + 'SELECT 1 FROM subscriptions s JOIN plans p ON p.id = s.plan_id ' + . 'WHERE s.user_id = :u AND s.status = "active" AND s.expires_at > NOW() ' + . 'AND p.code <> "free" AND p.price_cents > 0 LIMIT 1' + ); + $stmt->execute(['u' => $userId]); + return (bool)$stmt->fetchColumn(); + } + + public static function requireActiveMember(int $userId): void + { + if (!self::isActiveMember($userId)) { + Json::fail('membership_required', '此功能需要订阅后使用', 403); + } + } + + /** @return array> */ + public static function recentOrders(int $userId, int $limit = 8): array + { + return self::listOrders($userId, 1, $limit)['items']; + } + + /** + * @return array{items: array>, total: int, page: int, size: int} + */ + public static function listOrders(int $userId, int $page = 1, int $size = 8): array + { + $page = max(1, $page); + $size = max(1, min(50, $size)); + $offset = ($page - 1) * $size; + $pdo = Db::pdo(); + $countStmt = $pdo->prepare('SELECT COUNT(*) FROM pay_orders WHERE user_id = :u'); + $countStmt->execute(['u' => $userId]); + $total = (int)$countStmt->fetchColumn(); + $stmt = $pdo->prepare( + 'SELECT o.order_no, o.plan_id, o.amount_cents, o.status, o.channel, o.refund_status, ' + . 'o.created_at, o.paid_at, p.name AS plan_name, p.code AS plan_code ' + . 'FROM pay_orders o JOIN plans p ON p.id = o.plan_id ' + . 'WHERE o.user_id = :u ORDER BY o.id DESC LIMIT :lim OFFSET :off' + ); + $stmt->bindValue('u', $userId, \PDO::PARAM_INT); + $stmt->bindValue('lim', $size, \PDO::PARAM_INT); + $stmt->bindValue('off', $offset, \PDO::PARAM_INT); + $stmt->execute(); + $items = []; + foreach ($stmt->fetchAll() as $row) { + $items[] = [ + 'order_no' => $row['order_no'], + 'plan_id' => (int)$row['plan_id'], + 'plan_name' => $row['plan_name'], + 'plan_code' => $row['plan_code'], + 'amount_cents' => (int)$row['amount_cents'], + 'status' => $row['status'], + 'refund_status' => $row['refund_status'] ?? 'none', + 'channel' => $row['channel'], + 'created_at' => $row['created_at'], + 'paid_at' => $row['paid_at'], + ]; + } + return ['items' => $items, 'total' => $total, 'page' => $page, 'size' => $size]; + } + + public static function previewAllowed(int $userId): bool + { + return self::isActiveMember($userId); + } + + public static function settings(): array + { + $stmt = Db::pdo()->query('SELECT `key`, `value` FROM settings'); + $out = []; + foreach ($stmt->fetchAll() as $row) { + $out[$row['key']] = $row['value']; + } + return $out; + } + + public static function maxFilesLimit(int $userId): int + { + $plan = self::currentPlan($userId); + $limit = (int)($plan['max_files'] ?? 0); + return $limit > 0 ? $limit : 10; + } + + /** @param array $row */ + public static function enrichPlanForAdmin(array $row): array + { + $plan = self::enrichPlan($row); + $plan['is_active'] = (int)($row['is_active'] ?? 1); + return $plan; + } + + /** + * 由套餐数值字段生成权益说明(与 FileService / 订阅逻辑一致,可执行)。 + * @param array $row + * @return list + */ + /** @return list */ + public static function sanitizeFeatureLines(mixed $input): array + { + if (!is_array($input)) { + return []; + } + $out = []; + foreach ($input as $line) { + if (!is_string($line)) { + continue; + } + $t = trim($line); + if ($t === '') { + continue; + } + if (mb_strlen($t) > 200) { + $t = mb_substr($t, 0, 200); + } + $out[] = $t; + } + return array_values(array_unique($out)); + } + + /** @return list|null */ + public static function parseStoredFeatures(mixed $raw): ?array + { + if (is_array($raw)) { + $lines = self::sanitizeFeatureLines($raw); + return $lines === [] ? null : $lines; + } + if (!is_string($raw) || trim($raw) === '') { + return null; + } + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + return null; + } + $lines = self::sanitizeFeatureLines($decoded); + return $lines === [] ? null : $lines; + } + + /** + * @param list $coreLines + * @return list + */ + public static function buildPlanFeatures(string $code, int $durationDays, array $coreLines): array + { + $core = self::sanitizeFeatureLines($coreLines); + if ($code === 'free') { + return $core !== [] ? $core : self::planFeatureLines(['code' => 'free']); + } + $core = array_values(array_filter($core, static function (string $l): bool { + return strpos($l, '订阅有效期') === false + && strpos($l, '订阅周期') === false + && strpos($l, '到期后') === false + && strpos($l, '到期') === false; + })); + if ($core === []) { + $core = self::memberBenefitLines(); + } + if ($durationDays > 0) { + $core[] = '订阅周期:' . self::periodLabel($durationDays); + } + return $core; + } + + public static function planFeatureLines(array $row): array + { + $code = (string)($row['code'] ?? ''); + if ($code === 'free') { + return ['设计与编辑工具免费使用']; + } + $days = (int)($row['duration_days'] ?? 0); + return self::buildPlanFeatures($code, $days, self::memberBenefitLines()); + } + + public static function resolveFeatures(array $row): array + { + $stored = self::parseStoredFeatures($row['features'] ?? null); + if ($stored !== null) { + return $stored; + } + return self::planFeatureLines($row); + } + + /** @return list */ + public static function memberBenefitLines(): array + { + return [ + '高清预览', + '成品打印', + '云端保存', + '导出设计文件', + ]; + } + + /** @return list */ + public static function coreFeatureLines(array $features): array + { + return array_values(array_filter($features, static function (string $l): bool { + return strpos($l, '订阅有效期') === false + && strpos($l, '订阅周期') === false + && strpos($l, '到期后') === false + && strpos($l, '到期') === false; + })); + } + + /** @param array $row */ + private static function enrichPlan(array $row): array + { + $quotaMb = (int)($row['quota_mb'] ?? 0); + $durationDays = (int)($row['duration_days'] ?? 0); + $priceCents = (int)($row['price_cents'] ?? 0); + $maxFiles = (int)($row['max_files'] ?? 0); + return [ + 'id' => (int)($row['id'] ?? 0), + 'code' => (string)($row['code'] ?? ''), + 'name' => (string)($row['name'] ?? ''), + 'description' => (string)($row['description'] ?? ''), + 'price_cents' => $priceCents, + 'price_display' => number_format($priceCents / 100, 2, '.', ''), + 'quota_mb' => $quotaMb, + 'quota_display' => self::formatQuota($quotaMb), + 'max_files' => $maxFiles, + 'duration_days' => $durationDays, + 'period_label' => self::periodLabel($durationDays), + 'features' => self::resolveFeatures($row), + 'limits' => [ + 'quota_mb' => $quotaMb, + 'max_files' => $maxFiles, + 'duration_days' => $durationDays, + ], + 'sort_order' => (int)($row['sort_order'] ?? 0), + 'is_recommended' => (int)($row['is_recommended'] ?? 0) === 1, + 'is_active' => (int)($row['is_active'] ?? 1) === 1, + ]; + } + + /** @return array */ + private static function usageStats(int $userId): array + { + $pdo = Db::pdo(); + $fileStmt = $pdo->prepare( + 'SELECT COUNT(*) AS files_count, COALESCE(SUM(size), 0) AS storage_bytes ' + . 'FROM soon_files WHERE user_id = :u AND deleted_at IS NULL' + ); + $fileStmt->execute(['u' => $userId]); + $row = $fileStmt->fetch() ?: ['files_count' => 0, 'storage_bytes' => 0]; + $bytes = (int)$row['storage_bytes']; + $usedMb = $bytes > 0 ? round($bytes / 1024 / 1024, 2) : 0; + return [ + 'files_count' => (int)$row['files_count'], + 'storage_bytes' => $bytes, + 'used_mb' => $usedMb, + 'used_display' => self::formatQuota(max(1, $usedMb)) !== '0 MB' ? self::formatBytes($bytes) : '0 MB', + ]; + } + + private static function formatQuota(int $mb): string + { + if ($mb <= 0) { + return '0 MB'; + } + if ($mb >= 1024) { + $gb = $mb / 1024; + return ($gb >= 10 ? (string)(int)round($gb) : rtrim(rtrim(number_format($gb, 1, '.', ''), '0'), '.')) . ' GB'; + } + return $mb . ' MB'; + } + + private static function formatBytes(int $bytes): string + { + if ($bytes < 1024) { + return $bytes . ' B'; + } + if ($bytes < 1024 * 1024) { + return round($bytes / 1024, 1) . ' KB'; + } + return round($bytes / (1024 * 1024), 2) . ' MB'; + } + + private static function periodLabel(int $days): string + { + if ($days <= 0) { + return '永久有效'; + } + if ($days >= 365) { + return '1 年'; + } + if ($days === 90) { + return '1 季'; + } + if ($days >= 30 && $days % 30 === 0) { + $months = (int)($days / 30); + return $months . ' 个月'; + } + return $days . ' 天'; + } + + private static function daysUntil(string $expiresAt): ?int + { + if ($expiresAt === '') { + return null; + } + $ts = strtotime($expiresAt); + if ($ts === false) { + return null; + } + return max(0, (int)ceil(($ts - time()) / 86400)); + } +} diff --git a/backend-web/src/Services/PayService.php b/backend-web/src/Services/PayService.php new file mode 100644 index 0000000..824cb77 --- /dev/null +++ b/backend-web/src/Services/PayService.php @@ -0,0 +1,484 @@ +prepare( + 'INSERT INTO pay_orders (order_no, user_id, plan_id, channel, amount_cents, status, client_ip, created_at) ' + . 'VALUES (:o, :u, :p, :c, :a, "pending", :ip, :ts)' + ); + $stmt->execute([ + 'o' => $orderNo, + 'u' => $userId, + 'p' => $planId, + 'c' => $channel, + 'a' => $priceCents, + 'ip' => $clientIp, + 'ts' => date('Y-m-d H:i:s'), + ]); + $order = [ + 'id' => (int)Db::pdo()->lastInsertId(), + 'order_no' => $orderNo, + 'amount_cents' => $priceCents, + 'channel' => $channel, + 'status' => 'pending', + 'plan_id' => $planId, + ]; + return self::attachPaymentPayload($order, $plan, $clientIp, false); + } + + public static function checkoutOrderForUser(int $userId, string $orderNo, string $clientIp, ?string $channel = null): array + { + self::expireStalePendingOrders($userId); + $order = self::findByOrderNoForUser($orderNo, $userId); + if ($order === null) { + Json::fail('not_found', '订单不存在', 404); + } + if ($order['status'] !== 'pending') { + Json::fail('bad_request', '仅待支付订单可继续支付', 400); + } + if (self::isPendingExpired($order)) { + self::cancelPending((int)$order['id']); + Json::fail('gone', '订单已超时,请重新下单', 410); + } + if ($channel !== null && $channel !== $order['channel']) { + if (!in_array($channel, ['alipay', 'wechat'], true)) { + Json::fail('bad_request', '不支持的支付方式', 400); + } + Db::pdo()->prepare('UPDATE pay_orders SET channel = :c WHERE id = :id') + ->execute(['c' => $channel, 'id' => $order['id']]); + $order['channel'] = $channel; + } + $plan = self::loadActivePlan((int)$order['plan_id']); + return self::attachPaymentPayload($order, $plan, $clientIp, true); + } + + public static function cancelPendingForUser(int $userId, string $orderNo): bool + { + $order = self::findByOrderNoForUser($orderNo, $userId); + if ($order === null) { + Json::fail('not_found', '订单不存在', 404); + } + if ($order['status'] !== 'pending') { + Json::fail('bad_request', '仅待支付订单可取消', 400); + } + if (!self::cancelPending((int)$order['id'])) { + Json::fail('conflict', '订单状态已变更', 409); + } + return true; + } + + /** @return array */ + private static function loadActivePlan(int $planId): array + { + $stmt = Db::pdo()->prepare('SELECT * FROM plans WHERE id = :id AND is_active = 1'); + $stmt->execute(['id' => $planId]); + $plan = $stmt->fetch(); + if (!$plan) { + Json::fail('not_found', '套餐不存在', 404); + } + $priceCents = (int)$plan['price_cents']; + if ($priceCents <= 0) { + Json::fail('bad_request', '免费套餐无需支付', 400); + } + return $plan; + } + + private static function expireStalePendingOrders(int $userId): void + { + $cutoff = date('Y-m-d H:i:s', time() - self::PENDING_STALE_SECONDS); + $stmt = Db::pdo()->prepare( + 'SELECT id FROM pay_orders WHERE user_id = :u AND status = "pending" AND created_at < :ts' + ); + $stmt->execute(['u' => $userId, 'ts' => $cutoff]); + foreach ($stmt->fetchAll() as $row) { + self::cancelPending((int)$row['id']); + } + } + + /** @return array|null */ + private static function findReusablePendingOrder(int $userId, int $planId, string $channel): ?array + { + $cutoff = date('Y-m-d H:i:s', time() - self::PENDING_REUSE_SECONDS); + $stmt = Db::pdo()->prepare( + 'SELECT * FROM pay_orders WHERE user_id = :u AND plan_id = :p AND channel = :c ' + . 'AND status = "pending" AND created_at >= :ts ORDER BY id DESC LIMIT 1' + ); + $stmt->execute(['u' => $userId, 'p' => $planId, 'c' => $channel, 'ts' => $cutoff]); + $row = $stmt->fetch(); + return $row ?: null; + } + + private static function cancelPendingForPlan(int $userId, int $planId): void + { + $stmt = Db::pdo()->prepare( + 'SELECT id FROM pay_orders WHERE user_id = :u AND plan_id = :p AND status = "pending"' + ); + $stmt->execute(['u' => $userId, 'p' => $planId]); + foreach ($stmt->fetchAll() as $row) { + self::cancelPending((int)$row['id']); + } + } + + /** @param array $order */ + private static function isPendingExpired(array $order): bool + { + $created = strtotime((string)($order['created_at'] ?? '')); + if ($created <= 0) { + return true; + } + return (time() - $created) > self::PENDING_REUSE_SECONDS; + } + + /** + * @param array $order + * @param array $plan + * @return array + */ + private static function attachPaymentPayload(array $order, array $plan, string $clientIp, bool $reused): array + { + $channel = (string)$order['channel']; + $orderNo = (string)$order['order_no']; + $amountCents = (int)$order['amount_cents']; + $payload = [ + 'id' => (int)$order['id'], + 'order_no' => $orderNo, + 'amount_cents' => $amountCents, + 'channel' => $channel, + 'reused' => $reused, + 'plan' => [ + 'id' => (int)$plan['id'], + 'code' => $plan['code'], + 'name' => $plan['name'], + ], + ]; + if ($channel === 'alipay') { + $payload['pay_form'] = AlipayClient::pagePayForm( + $orderNo, + $amountCents, + 'SoonDesign ' . $plan['name'] + ); + } else { + $resp = WeChatClient::nativeOrder( + $orderNo, + $amountCents, + 'SoonDesign ' . $plan['name'], + $clientIp + ); + $payload['qrcode'] = $resp['qrcode']; + } + return $payload; + } + + public static function markPaid(string $orderNo, string $channel, string $txnId, ?int $amountCents = null): bool + { + $pdo = Db::pdo(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('SELECT * FROM pay_orders WHERE order_no = :o FOR UPDATE'); + $stmt->execute(['o' => $orderNo]); + $order = $stmt->fetch(); + if (!$order) { + $pdo->rollBack(); + return false; + } + if ($order['status'] === 'paid') { + $pdo->commit(); + return true; + } + if ($order['channel'] !== $channel) { + $pdo->rollBack(); + return false; + } + if ($amountCents !== null && (int)$order['amount_cents'] !== $amountCents) { + $pdo->rollBack(); + return false; + } + $planStmt = $pdo->prepare('SELECT * FROM plans WHERE id = :id'); + $planStmt->execute(['id' => $order['plan_id']]); + $plan = $planStmt->fetch(); + if (!$plan) { + $pdo->rollBack(); + return false; + } + $pdo->prepare('UPDATE pay_orders SET status = "paid", txn_id = :t, paid_at = :ts WHERE id = :id') + ->execute(['t' => $txnId, 'ts' => date('Y-m-d H:i:s'), 'id' => $order['id']]); + $baseTs = time(); + $activeStmt = $pdo->prepare( + 'SELECT expires_at FROM subscriptions WHERE user_id = :u AND status = "active" AND expires_at > NOW() ' + . 'ORDER BY expires_at DESC LIMIT 1' + ); + $activeStmt->execute(['u' => $order['user_id']]); + $activeExpires = $activeStmt->fetchColumn(); + if ($activeExpires) { + $baseTs = max($baseTs, strtotime((string)$activeExpires)); + } + $expires = date('Y-m-d H:i:s', $baseTs + (int)$plan['duration_days'] * 86400); + $pdo->prepare('UPDATE subscriptions SET status = "expired" WHERE user_id = :u AND status = "active"') + ->execute(['u' => $order['user_id']]); + $pdo->prepare( + 'INSERT INTO subscriptions (user_id, plan_id, source_order_id, status, started_at, expires_at) ' + . 'VALUES (:u, :p, :oid, "active", :sa, :ea)' + )->execute([ + 'u' => $order['user_id'], + 'p' => $order['plan_id'], + 'oid' => $order['id'], + 'sa' => date('Y-m-d H:i:s'), + 'ea' => $expires, + ]); + $pdo->commit(); + return true; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } + + public static function findByOrderNo(string $orderNo): ?array + { + $stmt = Db::pdo()->prepare('SELECT * FROM pay_orders WHERE order_no = :o'); + $stmt->execute(['o' => $orderNo]); + $row = $stmt->fetch(); + return $row ?: null; + } + + public static function findByOrderNoForUser(string $orderNo, int $userId): ?array + { + $order = self::findByOrderNo($orderNo); + if ($order === null || (int)$order['user_id'] !== $userId) { + return null; + } + return $order; + } + + public static function findById(int $id): ?array + { + $stmt = Db::pdo()->prepare('SELECT * FROM pay_orders WHERE id = :id'); + $stmt->execute(['id' => $id]); + $row = $stmt->fetch(); + return $row ?: null; + } + + public static function cancelPending(int $orderId): bool + { + $pdo = Db::pdo(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('SELECT * FROM pay_orders WHERE id = :id FOR UPDATE'); + $stmt->execute(['id' => $orderId]); + $order = $stmt->fetch(); + if (!$order || $order['status'] !== 'pending') { + $pdo->rollBack(); + return false; + } + $ts = date('Y-m-d H:i:s'); + $pdo->prepare('UPDATE pay_orders SET status = "cancelled", cancelled_at = :ts WHERE id = :id') + ->execute(['ts' => $ts, 'id' => $orderId]); + $pdo->commit(); + return true; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } + + public static function requestRefund(int $userId, string $orderNo, string $reason): bool + { + $reason = trim($reason); + if ($reason === '') { + Json::fail('bad_request', '请填写退款原因', 400); + } + $pdo = Db::pdo(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('SELECT * FROM pay_orders WHERE order_no = :o AND user_id = :u FOR UPDATE'); + $stmt->execute(['o' => $orderNo, 'u' => $userId]); + $order = $stmt->fetch(); + if (!$order) { + $pdo->rollBack(); + Json::fail('not_found', '订单不存在', 404); + } + if ($order['status'] !== 'paid') { + $pdo->rollBack(); + Json::fail('bad_request', '仅已支付订单可申请退款', 400); + } + if ($order['refund_status'] === 'pending') { + $pdo->rollBack(); + Json::fail('conflict', '退款申请已在审核中', 409); + } + if ($order['refund_status'] === 'approved' || $order['status'] === 'refunded') { + $pdo->rollBack(); + Json::fail('conflict', '订单已退款', 409); + } + $pdo->prepare( + 'UPDATE pay_orders SET refund_status = "pending", refund_reason = :r, refund_note = NULL WHERE id = :id' + )->execute(['r' => $reason, 'id' => $order['id']]); + $pdo->commit(); + return true; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } + + /** @return array{ok: bool, gateway: string} */ + public static function approveRefund(int $orderId, ?string $adminNote = null): array + { + $pdo = Db::pdo(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('SELECT * FROM pay_orders WHERE id = :id FOR UPDATE'); + $stmt->execute(['id' => $orderId]); + $order = $stmt->fetch(); + if (!$order) { + $pdo->rollBack(); + Json::fail('not_found', '订单不存在', 404); + } + if ($order['status'] !== 'paid') { + $pdo->rollBack(); + Json::fail('bad_request', '仅已支付订单可退款', 400); + } + if ($order['refund_status'] === 'approved') { + $pdo->rollBack(); + Json::fail('conflict', '订单已退款', 409); + } + if ($order['refund_status'] === 'rejected') { + $pdo->rollBack(); + Json::fail('bad_request', '退款申请已被拒绝,请让用户重新申请', 400); + } + if (!in_array($order['refund_status'], ['none', 'pending'], true)) { + $pdo->rollBack(); + Json::fail('bad_request', '当前状态不可退款', 400); + } + + $gateway = self::tryGatewayRefund($order); + $ts = date('Y-m-d H:i:s'); + $pdo->prepare( + 'UPDATE pay_orders SET status = "refunded", refund_status = "approved", refund_note = :n, refunded_at = :ts ' + . 'WHERE id = :id' + )->execute([ + 'n' => $adminNote !== null && $adminNote !== '' ? $adminNote : null, + 'ts' => $ts, + 'id' => $orderId, + ]); + self::revokeSubscriptionForOrder($pdo, $order); + $pdo->commit(); + return ['ok' => true, 'gateway' => $gateway]; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } + + public static function rejectRefund(int $orderId, string $adminNote): bool + { + $adminNote = trim($adminNote); + if ($adminNote === '') { + Json::fail('bad_request', '请填写拒绝原因', 400); + } + $pdo = Db::pdo(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare('SELECT * FROM pay_orders WHERE id = :id FOR UPDATE'); + $stmt->execute(['id' => $orderId]); + $order = $stmt->fetch(); + if (!$order) { + $pdo->rollBack(); + Json::fail('not_found', '订单不存在', 404); + } + if ($order['status'] !== 'paid' || $order['refund_status'] !== 'pending') { + $pdo->rollBack(); + Json::fail('bad_request', '无待审核的退款申请', 400); + } + $pdo->prepare('UPDATE pay_orders SET refund_status = "rejected", refund_note = :n WHERE id = :id') + ->execute(['n' => $adminNote, 'id' => $orderId]); + $pdo->commit(); + return true; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } + + private static function revokeSubscriptionForOrder(\PDO $pdo, array $order): void + { + $oid = (int)$order['id']; + $uid = (int)$order['user_id']; + $pid = (int)$order['plan_id']; + $subStmt = $pdo->prepare( + 'SELECT id FROM subscriptions WHERE source_order_id = :oid AND status = "active" LIMIT 1' + ); + $subStmt->execute(['oid' => $oid]); + $subId = $subStmt->fetchColumn(); + if (!$subId) { + $fallback = $pdo->prepare( + 'SELECT id FROM subscriptions WHERE user_id = :u AND plan_id = :p AND status = "active" ' + . 'ORDER BY id DESC LIMIT 1' + ); + $fallback->execute(['u' => $uid, 'p' => $pid]); + $subId = $fallback->fetchColumn(); + } + if ($subId) { + $pdo->prepare('UPDATE subscriptions SET status = "cancelled" WHERE id = :id') + ->execute(['id' => $subId]); + } + } + + private static function tryGatewayRefund(array $order): string + { + if ($order['channel'] === 'alipay') { + $cfg = Config::get('alipay', []); + if (empty($cfg['private_key']) || empty($cfg['app_id'])) { + return 'skipped_no_keys'; + } + return AlipayClient::refund($order['order_no'], (int)$order['amount_cents']) ? 'alipay_ok' : 'alipay_fail'; + } + if ($order['channel'] === 'wechat') { + $cfg = Config::get('wechat', []); + if (empty($cfg['mch_private_key']) || empty($cfg['mch_id'])) { + return 'skipped_no_keys'; + } + return WeChatClient::refund( + $order['order_no'], + (int)$order['amount_cents'], + (string)($order['txn_id'] ?? '') + ) ? 'wechat_ok' : 'wechat_fail'; + } + return 'unknown_channel'; + } +} diff --git a/backend-web/src/Services/PaymentConfigLoader.php b/backend-web/src/Services/PaymentConfigLoader.php new file mode 100644 index 0000000..cbf63a4 --- /dev/null +++ b/backend-web/src/Services/PaymentConfigLoader.php @@ -0,0 +1,41 @@ + ['alipay', 'private_key'], + 'alipay_public_key.pem' => ['alipay', 'public_key'], + 'wechat_mch_private_key.pem' => ['wechat', 'mch_private_key'], + 'wechat_api_v3_key.pem' => ['wechat', 'api_v3_key'], + 'wechat_platform_cert.pem' => ['wechat', 'platform_cert'], + ]; + + foreach ($map as $file => [$section, $key]) { + $path = $dir . '/' . $file; + if (!is_file($path)) { + continue; + } + $content = trim((string)file_get_contents($path)); + if ($content === '') { + continue; + } + if (!isset($config[$section]) || !is_array($config[$section])) { + $config[$section] = []; + } + $config[$section][$key] = $content; + } + } +} diff --git a/backend-web/src/Services/WeChatPay/AesGcm.php b/backend-web/src/Services/WeChatPay/AesGcm.php new file mode 100644 index 0000000..6eee3c7 --- /dev/null +++ b/backend-web/src/Services/WeChatPay/AesGcm.php @@ -0,0 +1,33 @@ + true, + CURLOPT_HTTPHEADER => [ + 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $auth, + 'Accept: application/json', + ], + CURLOPT_TIMEOUT => 15, + ]); + $resp = (string)curl_exec($ch); + curl_close($ch); + $data = json_decode($resp, true); + if (!is_array($data) || !isset($data['data'])) { + return null; + } + foreach ($data['data'] as $cert) { + if (($cert['serial_no'] ?? '') === $serial) { + $enc = $cert['encrypt_certificate'] ?? null; + if (!is_array($enc)) return null; + $plain = AesGcm::decrypt( + base64_decode((string)$enc['ciphertext'], true), + (string)$enc['associated_data'], + (string)$enc['nonce'], + (string)($cfg['api_v3_key'] ?? '') + ); + return $plain === null ? null : $plain; + } + } + return null; + } + + private static function sign(string $url, array $cfg): string + { + $message = "GET\n" . $url . "\n" . time() . "\n" . bin2hex(random_bytes(8)) . "\n\n"; + openssl_sign($message, $sig, (string)($cfg['mch_private_key'] ?? ''), OPENSSL_ALGO_SHA256); + return base64_encode($sig); + } +} diff --git a/backend-web/src/Services/WeChatPay/Client.php b/backend-web/src/Services/WeChatPay/Client.php new file mode 100644 index 0000000..b1bcd4a --- /dev/null +++ b/backend-web/src/Services/WeChatPay/Client.php @@ -0,0 +1,134 @@ + $mchId, + 'out_trade_no' => $orderNo, + 'appid' => (string)($cfg['app_id'] ?? ''), + 'description' => $description, + 'notify_url' => (string)Config::get('site.base_url', '') . '/api/v1/pay/wechat/notify', + 'amount' => ['total' => $amountCents, 'currency' => 'CNY'], + ], JSON_UNESCAPED_UNICODE); + $token = self::auth('POST', $url, $body, (string)($cfg['mch_private_key'] ?? '')); + $resp = self::http('POST', $url, $body, $token); + $data = json_decode($resp['body'], true); + return [ + 'http_status' => $resp['status'], + 'qrcode' => $data['code_url'] ?? null, + 'raw' => $data, + ]; + } + + public static function verifyNotify(string $body, string $signature, string $serial, string $timestamp, string $nonce): bool + { + $cert = CertCache::get($serial); + if ($cert === null) { + $cfg = Config::get('wechat', []); + $cert = (string)($cfg['platform_cert'] ?? ''); + if ($cert === '') { + return false; + } + } + return Signature::verifyNotify('POST', '/api/v1/pay/wechat/notify', $timestamp, $nonce, $body, $signature, $cert); + } + + public static function decryptResource(string $ciphertext, string $associatedData, string $nonce, string $apiV3Key): ?string + { + $raw = base64_decode($ciphertext, true); + if ($raw === false) { + return null; + } + return AesGcm::decrypt($raw, $associatedData, $nonce, $apiV3Key); + } + + private static function auth(string $method, string $url, string $body, string $privateKey): string + { + $cfg = Config::get('wechat', []); + $mchId = (string)($cfg['mch_id'] ?? ''); + $serial = (string)($cfg['mch_serial_no'] ?? ''); + $nonceStr = bin2hex(random_bytes(8)); + $timestamp = (string)time(); + $message = $method . "\n" . $url . "\n" . $timestamp . "\n" . $nonceStr . "\n" . $body . "\n"; + openssl_sign($message, $sig, $privateKey, OPENSSL_ALGO_SHA256); + $signature = base64_encode($sig); + $token = sprintf( + 'mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"', + $mchId, $nonceStr, $timestamp, $serial, $signature + ); + return 'WECHATPAY2-SHA256-RSA2048 ' . $token; + } + + public static function refund(string $orderNo, int $amountCents, string $txnId): bool + { + if ($txnId === '') { + return false; + } + $cfg = Config::get('wechat', []); + $mchId = (string)($cfg['mch_id'] ?? ''); + $privateKey = (string)($cfg['mch_private_key'] ?? ''); + if ($mchId === '' || $privateKey === '') { + return false; + } + $url = '/v3/refund/domestic/refunds'; + $outRefundNo = 'RF' . $orderNo . random_int(100, 999); + $body = json_encode([ + 'transaction_id' => $txnId, + 'out_refund_no' => $outRefundNo, + 'reason' => '管理员审核退款', + 'amount' => [ + 'refund' => $amountCents, + 'total' => $amountCents, + 'currency' => 'CNY', + ], + ], JSON_UNESCAPED_UNICODE); + $token = self::auth('POST', $url, $body, $privateKey); + $resp = self::http('POST', $url, $body, $token); + if ($resp['status'] < 200 || $resp['status'] >= 300) { + return false; + } + $data = json_decode($resp['body'], true); + return is_array($data) && !empty($data['refund_id']); + } + + private static function http(string $method, string $url, string $body, string $auth): array + { + $ch = curl_init(self::baseUrl() . $url); + $headers = [ + 'Authorization: ' . $auth, + 'Content-Type: application/json', + 'Accept: application/json', + 'User-Agent: SoonDesign/1.0', + ]; + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $body, + CURLOPT_TIMEOUT => 20, + ]); + $resp = curl_exec($ch); + $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return ['status' => $status, 'body' => (string)$resp]; + } +} diff --git a/backend-web/src/Services/WeChatPay/Signature.php b/backend-web/src/Services/WeChatPay/Signature.php new file mode 100644 index 0000000..5879f71 --- /dev/null +++ b/backend-web/src/Services/WeChatPay/Signature.php @@ -0,0 +1,48 @@ + 'config_missing', + 'message' => '未找到 backend-web/config/local.php,请先写入生产配置。', + ], JSON_UNESCAPED_UNICODE); + exit; + } +} + +if (!is_array($config)) { + http_response_code(500); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['error' => 'config_invalid'], JSON_UNESCAPED_UNICODE); + exit; +} + +if (!isset($config['app']) || !is_array($config['app'])) { + $config['app'] = []; +} +if (!isset($config['app']['env'])) { + $config['app']['env'] = 'production'; +} +if (!isset($config['app']['debug'])) { + $config['app']['debug'] = false; +} + +if ($config['app']['debug']) { + ini_set('display_errors', '1'); + ini_set('display_startup_errors', '1'); + error_reporting(E_ALL); +} else { + ini_set('display_errors', '0'); + error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED); +} + +date_default_timezone_set($config['app']['timezone'] ?? 'Asia/Shanghai'); + +spl_autoload_register(static function (string $class): void { + $prefixes = [ + 'Soon\\Api\\' => SOON_API_ROOT . '/', + 'Soon\\Admin\\' => SOON_API_ROOT . '/', + ]; + foreach ($prefixes as $prefix => $baseDir) { + if (strpos($class, $prefix) !== 0) { + continue; + } + $relative = substr($class, strlen($prefix)); + $path = $baseDir . str_replace('\\', '/', $relative) . '.php'; + if (is_file($path)) { + require $path; + return; + } + } +}); + +require SOON_API_ROOT . '/Core/Config.php'; +\Soon\Api\Core\Config::init($config); +require SOON_API_ROOT . '/Services/PaymentConfigLoader.php'; +\Soon\Api\Services\PaymentConfigLoader::merge($config); +\Soon\Api\Core\Config::init($config); diff --git a/backend-web/storage/cache/rate/.gitkeep b/backend-web/storage/cache/rate/.gitkeep new file mode 100644 index 0000000..967d856 --- /dev/null +++ b/backend-web/storage/cache/rate/.gitkeep @@ -0,0 +1 @@ +# 限速桶目录,运行时自动写入 .json。 diff --git a/backend-web/storage/cache/rate/3907ccd67c518764ef7b8ef069439ddf.json b/backend-web/storage/cache/rate/3907ccd67c518764ef7b8ef069439ddf.json new file mode 100644 index 0000000..82cb6bd --- /dev/null +++ b/backend-web/storage/cache/rate/3907ccd67c518764ef7b8ef069439ddf.json @@ -0,0 +1 @@ +{"start":1780902406,"count":11} \ No newline at end of file diff --git a/backend-web/storage/cache/rate/3e8125a127ab317983e07c575f6fa30d.json b/backend-web/storage/cache/rate/3e8125a127ab317983e07c575f6fa30d.json new file mode 100644 index 0000000..53e5740 --- /dev/null +++ b/backend-web/storage/cache/rate/3e8125a127ab317983e07c575f6fa30d.json @@ -0,0 +1 @@ +{"start":1780911074,"count":3} \ No newline at end of file diff --git a/backend-web/storage/cache/rate/7e272c43563d8aa7be34d576076409a6.json b/backend-web/storage/cache/rate/7e272c43563d8aa7be34d576076409a6.json new file mode 100644 index 0000000..fa2e952 --- /dev/null +++ b/backend-web/storage/cache/rate/7e272c43563d8aa7be34d576076409a6.json @@ -0,0 +1 @@ +{"start":1780907292,"count":2} \ No newline at end of file diff --git a/backend-web/storage/cache/rate/c493174a678067c61131069dbaadd56d.json b/backend-web/storage/cache/rate/c493174a678067c61131069dbaadd56d.json new file mode 100644 index 0000000..4daa8be --- /dev/null +++ b/backend-web/storage/cache/rate/c493174a678067c61131069dbaadd56d.json @@ -0,0 +1 @@ +{"start":1780908575,"count":1} \ No newline at end of file diff --git a/backend-web/storage/cache/rate/e830ce08b1005f32f93287d5e3332c11.json b/backend-web/storage/cache/rate/e830ce08b1005f32f93287d5e3332c11.json new file mode 100644 index 0000000..a61b758 --- /dev/null +++ b/backend-web/storage/cache/rate/e830ce08b1005f32f93287d5e3332c11.json @@ -0,0 +1 @@ +{"start":1780893461,"count":1} \ No newline at end of file diff --git a/backend-web/storage/cache/wechat_certs/.gitkeep b/backend-web/storage/cache/wechat_certs/.gitkeep new file mode 100644 index 0000000..4fc6427 --- /dev/null +++ b/backend-web/storage/cache/wechat_certs/.gitkeep @@ -0,0 +1 @@ +# WeChat 平台证书缓存目录。 diff --git a/backend-web/storage/logs/.gitkeep b/backend-web/storage/logs/.gitkeep new file mode 100644 index 0000000..c9b04f9 --- /dev/null +++ b/backend-web/storage/logs/.gitkeep @@ -0,0 +1 @@ +# 通用日志目录(保留供 bootstrap.php 调用)。 diff --git a/backend-web/storage/payment/.gitkeep b/backend-web/storage/payment/.gitkeep new file mode 100644 index 0000000..2d4af96 --- /dev/null +++ b/backend-web/storage/payment/.gitkeep @@ -0,0 +1,2 @@ +# 支付私钥 / 公钥 / 平台证书存储目录。绝不进入 git 与 pack zip。 +# 文件权限 0600,目录权限 0700。 diff --git a/backend-web/storage/users/.gitkeep b/backend-web/storage/users/.gitkeep new file mode 100644 index 0000000..c92887a --- /dev/null +++ b/backend-web/storage/users/.gitkeep @@ -0,0 +1,2 @@ +# 用户 .soon 文件按 user_id 分目录存储。 +# 部署时确保此目录可写:chown -R www:www backend/storage diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..696c635 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,9 @@ +MYSQL_ROOT_PASSWORD=rootlocal +MYSQL_DATABASE=soondesign +MYSQL_USER=soondesign +MYSQL_PASSWORD=soondesign +WEB_PORT=8100 +ELECTRON_PORT=8101 +BACKEND_PORT=8102 +SOON_DEV_ADMIN_EMAIL=admin@local.test +SOON_DEV_ADMIN_PASS=admin123 diff --git a/docker/.gitattributes b/docker/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/docker/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..6c1d2dd --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,67 @@ +name: soondesign + +services: + mysql: + image: mysql:8.0 + container_name: soondesign-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootlocal} + MYSQL_DATABASE: ${MYSQL_DATABASE:-soondesign} + MYSQL_USER: ${MYSQL_USER:-soondesign} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-soondesign} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + volumes: + - soondesign_mysql_data:/var/lib/mysql + - ../backend-web/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-rootlocal}"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 30s + + api: + image: php:8.2-cli-bookworm + container_name: soondesign-api + restart: unless-stopped + ports: + - "${BACKEND_PORT:-8102}:8080" + environment: + SOON_DB_HOST: mysql + SOON_DB_PORT: 3306 + SOON_DB_NAME: ${MYSQL_DATABASE:-soondesign} + SOON_DB_USER: ${MYSQL_USER:-soondesign} + SOON_DB_PASS: ${MYSQL_PASSWORD:-soondesign} + SOON_DEV_ADMIN_EMAIL: ${SOON_DEV_ADMIN_EMAIL:-admin@local.test} + SOON_DEV_ADMIN_PASS: ${SOON_DEV_ADMIN_PASS:-admin123} + volumes: + - ../backend-web:/var/www + - ./config/local.php:/var/www/config/local.php:ro + - ../soonModels:/var/soonModels + - ./php:/docker/php:ro + entrypoint: ["/bin/sh", "-c", "tr -d '\\r' < /docker/php/entrypoint-dev.sh | sh"] + depends_on: + mysql: + condition: service_healthy + + frontend: + image: nginx:1.27-alpine + container_name: soondesign-frontend + restart: unless-stopped + ports: + - "${WEB_PORT:-8100}:8080" + - "${ELECTRON_PORT:-8101}:8081" + volumes: + - ../frontend-web:/var/www/web:ro + - ../frontend-electron:/var/www/electron:ro + - ./config/deploy-config.js:/var/www/web/assets/deploy-config.js:ro + - ./nginx/frontend.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - api + +volumes: + soondesign_mysql_data: + name: soondesign_mysql_data diff --git a/docker/nginx/frontend.conf b/docker/nginx/frontend.conf new file mode 100644 index 0000000..ba3a30c --- /dev/null +++ b/docker/nginx/frontend.conf @@ -0,0 +1,29 @@ +server { + listen 8080; + server_name localhost; + root /var/www/web; + index pages/index.web.html; + charset utf-8; + absolute_redirect off; + client_max_body_size 32m; + location = / { return 302 /pages/index.web.html; } + location = /pages { return 302 /pages/index.web.html; } + location = /pages/ { return 302 /pages/index.web.html; } + location ~* \.(js|css)$ { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri =404; + } + location / { try_files $uri $uri/ =404; } +} + +server { + listen 8081; + server_name localhost; + root /var/www/electron; + index pages/index.html; + charset utf-8; + absolute_redirect off; + client_max_body_size 32m; + location = / { return 302 /pages/index.html; } + location / { try_files $uri $uri/ =404; } +} diff --git a/docker/php/entrypoint-dev.sh b/docker/php/entrypoint-dev.sh new file mode 100644 index 0000000..0219d9c --- /dev/null +++ b/docker/php/entrypoint-dev.sh @@ -0,0 +1,44 @@ +#!/bin/sh +set -e + +if ! php -m 2>/dev/null | grep -q pdo_mysql; then + echo "[api] installing pdo_mysql..." + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ + libmariadb-dev-compat $PHPIZE_DEPS + docker-php-ext-install -j"$(nproc)" pdo_mysql + apt-get purge -y --auto-remove $PHPIZE_DEPS + rm -rf /var/lib/apt/lists/* +fi + +host="${SOON_DB_HOST:-mysql}" +port="${SOON_DB_PORT:-3306}" +user="${SOON_DB_USER:-soondesign}" +pass="${SOON_DB_PASS:-soondesign}" +db="${SOON_DB_NAME:-soondesign}" + +i=0 +while [ "$i" -lt 60 ]; do + if php -r " + try { + new PDO( + 'mysql:host=${host};port=${port};dbname=${db};charset=utf8mb4', + '${user}', + '${pass}', + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + exit(0); + } catch (Throwable \$e) { + exit(1); + } + "; then + break + fi + i=$((i + 1)) + sleep 2 +done + +php /docker/php/migrate-dev-schema.php || true +php /docker/php/seed-dev-admin.php || true + +exec php -S 0.0.0.0:8080 -t /var/www/public /docker/php/router-dev.php diff --git a/docker/php/migrate-dev-schema.php b/docker/php/migrate-dev-schema.php new file mode 100644 index 0000000..945c4a9 --- /dev/null +++ b/docker/php/migrate-dev-schema.php @@ -0,0 +1,141 @@ + PDO::ERRMODE_EXCEPTION] + ); +} catch (Throwable $e) { + fwrite(STDERR, 'migrate-dev-schema: ' . $e->getMessage() . PHP_EOL); + exit(1); +} + +function columnExists(PDO $pdo, string $table, string $column): bool +{ + $stmt = $pdo->prepare( + 'SELECT COUNT(*) FROM information_schema.COLUMNS ' + . 'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t AND COLUMN_NAME = :c' + ); + $stmt->execute(['t' => $table, 'c' => $column]); + return (int)$stmt->fetchColumn() > 0; +} + +$alters = []; +if (!columnExists($pdo, 'subscriptions', 'source_order_id')) { + $alters[] = 'ALTER TABLE subscriptions ADD COLUMN source_order_id BIGINT UNSIGNED DEFAULT NULL AFTER plan_id'; + $alters[] = 'ALTER TABLE subscriptions ADD KEY source_order (source_order_id)'; +} +if (!columnExists($pdo, 'pay_orders', 'refund_status')) { + $alters[] = "ALTER TABLE pay_orders ADD COLUMN refund_status ENUM('none','pending','approved','rejected') NOT NULL DEFAULT 'none' AFTER status"; + $alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_reason TEXT DEFAULT NULL AFTER refund_status'; + $alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_note TEXT DEFAULT NULL AFTER refund_reason'; + $alters[] = 'ALTER TABLE pay_orders ADD COLUMN cancelled_at DATETIME DEFAULT NULL AFTER paid_at'; + $alters[] = 'ALTER TABLE pay_orders ADD COLUMN refunded_at DATETIME DEFAULT NULL AFTER cancelled_at'; + $alters[] = 'ALTER TABLE pay_orders ADD KEY refund_status (refund_status)'; +} + +if (!columnExists($pdo, 'plans', 'description')) { + $alters[] = 'ALTER TABLE plans ADD COLUMN description VARCHAR(255) DEFAULT NULL AFTER name'; + $alters[] = 'ALTER TABLE plans ADD COLUMN max_files INT UNSIGNED NOT NULL DEFAULT 0 AFTER quota_mb'; + $alters[] = 'ALTER TABLE plans ADD COLUMN features TEXT DEFAULT NULL AFTER duration_days'; + $alters[] = 'ALTER TABLE plans ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER features'; + $alters[] = 'ALTER TABLE plans ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0 AFTER sort_order'; +} + +if (!columnExists($pdo, 'users', 'admin_level')) { + $alters[] = "ALTER TABLE users ADD COLUMN admin_level ENUM('full','ops') DEFAULT NULL AFTER role"; +} + +foreach ($alters as $sql) { + $pdo->exec($sql); + fwrite(STDOUT, "migrate-dev-schema: {$sql}" . PHP_EOL); +} + +if (columnExists($pdo, 'users', 'admin_level')) { + $pdo->exec( + "UPDATE users SET admin_level = 'full' WHERE role = 'admin' AND (admin_level IS NULL OR admin_level = '')" + ); + fwrite(STDOUT, 'migrate-dev-schema: admin_level backfill for existing admins' . PHP_EOL); +} + +if (columnExists($pdo, 'plans', 'description')) { + $legacyCodes = ['pro_monthly' => 'member_monthly', 'pro_yearly' => 'member_yearly']; + foreach ($legacyCodes as $oldCode => $newCode) { + $stmt = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1'); + $stmt->execute(['c' => $oldCode]); + if ($stmt->fetch()) { + $conflict = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1'); + $conflict->execute(['c' => $newCode]); + if ($conflict->fetch()) { + $pdo->prepare('UPDATE plans SET is_active = 0 WHERE code = :c')->execute(['c' => $oldCode]); + } else { + $pdo->prepare('UPDATE plans SET code = :new WHERE code = :old')->execute(['new' => $newCode, 'old' => $oldCode]); + } + } + } + + $quota = 2048; + $maxFiles = 200; + $seed = [ + ['free', '免费版', '免费体验设计与编辑', 0, $quota, $maxFiles, 0, + '["设计与编辑工具免费使用"]', 0, 0, 1], + ['member_monthly', '月度订阅', '按月灵活使用,随时续订', 1999, $quota, $maxFiles, 30, + '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]', 10, 1, 1], + ['member_quarterly', '季度订阅', '连续三个月,更省心', 5299, $quota, $maxFiles, 90, + '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]', 15, 0, 1], + ['member_yearly', '年度订阅', '全年畅享,性价比更高', 19999, $quota, $maxFiles, 365, + '["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]', 20, 0, 1], + ]; + $upsert = $pdo->prepare( + 'INSERT INTO plans (code, name, description, price_cents, quota_mb, max_files, duration_days, features, sort_order, is_recommended, is_active) ' + . 'VALUES (:c, :n, :d, :p, :q, :mf, :dd, :f, :so, :ir, :a) ' + . 'ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), price_cents=VALUES(price_cents), ' + . 'quota_mb=VALUES(quota_mb), max_files=VALUES(max_files), duration_days=VALUES(duration_days), ' + . 'sort_order=VALUES(sort_order), is_recommended=VALUES(is_recommended), is_active=VALUES(is_active)' + ); + foreach ($seed as $row) { + $upsert->execute([ + 'c' => $row[0], 'n' => $row[1], 'd' => $row[2], 'p' => $row[3], 'q' => $row[4], + 'mf' => $row[5], 'dd' => $row[6], 'f' => $row[7], 'so' => $row[8], 'ir' => $row[9], 'a' => $row[10], + ]); + } + fwrite(STDOUT, 'migrate-dev-schema: membership plans catalog synced' . PHP_EOL); + + $legacyFeatStmt = $pdo->query( + 'SELECT code, duration_days FROM plans WHERE features LIKE \'%预览成品效果%\' ' + . 'OR features LIKE \'%到期后恢复为非会员%\' OR features LIKE \'%会员权益一致%\' ' + . 'OR name LIKE \'%会员 ·%\'' + ); + $featUpd = $pdo->prepare('UPDATE plans SET features = :f WHERE code = :c'); + $coreFeatures = ['高清预览', '成品打印', '云端保存', '导出设计文件']; + foreach ($legacyFeatStmt->fetchAll() as $legacyRow) { + $code = (string)$legacyRow['code']; + $days = (int)$legacyRow['duration_days']; + if ($code === 'free') { + $featUpd->execute([ + 'f' => '["设计与编辑工具免费使用"]', + 'c' => $code, + ]); + continue; + } + $lines = $coreFeatures; + if ($days > 0) { + $period = $days >= 365 ? '1 年' : ($days === 90 ? '1 季' : ($days >= 30 && $days % 30 === 0 ? ((int)($days / 30)) . ' 个月' : $days . ' 天')); + $lines[] = '订阅周期:' . $period; + } + $featUpd->execute([ + 'f' => json_encode($lines, JSON_UNESCAPED_UNICODE), + 'c' => $code, + ]); + } + fwrite(STDOUT, 'migrate-dev-schema: legacy plan copy refreshed' . PHP_EOL); +} diff --git a/docker/php/router-dev.php b/docker/php/router-dev.php new file mode 100644 index 0000000..a606d33 --- /dev/null +++ b/docker/php/router-dev.php @@ -0,0 +1,31 @@ + false, 'error' => 'not_found', 'message' => 'Not found'], JSON_UNESCAPED_UNICODE); +return true; diff --git a/docker/php/seed-dev-admin.php b/docker/php/seed-dev-admin.php new file mode 100644 index 0000000..422191e --- /dev/null +++ b/docker/php/seed-dev-admin.php @@ -0,0 +1,44 @@ + PDO::ERRMODE_EXCEPTION] + ); +} catch (Throwable $e) { + fwrite(STDERR, 'seed-dev-admin: ' . $e->getMessage() . PHP_EOL); + exit(1); +} + +$stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1'); +$stmt->execute(['email' => $email]); +if ($stmt->fetchColumn()) { + exit(0); +} + +$hash = password_hash($plain, PASSWORD_BCRYPT); +$ins = $pdo->prepare( + 'INSERT INTO users (email, password_hash, role, admin_level, status, created_at) ' + . 'VALUES (:email, :hash, :role, :level, :status, :created_at)' +); +$ins->execute([ + 'email' => $email, + 'hash' => $hash, + 'role' => 'admin', + 'level' => 'full', + 'status' => 'active', + 'created_at' => date('Y-m-d H:i:s'), +]); + +fwrite(STDOUT, "seed-dev-admin: created {$email}" . PHP_EOL); diff --git a/docker/sync-config.ps1 b/docker/sync-config.ps1 new file mode 100644 index 0000000..a1aa0d1 --- /dev/null +++ b/docker/sync-config.ps1 @@ -0,0 +1,70 @@ +param([string]$EnvFile = "$PSScriptRoot\.env", [string]$Root = (Split-Path $PSScriptRoot -Parent)) + +if (-not (Test-Path $EnvFile)) { Copy-Item "$PSScriptRoot\.env.example" $EnvFile } + +$ports = @{ WEB_PORT = '8100'; ELECTRON_PORT = '8101'; BACKEND_PORT = '8102' } +Get-Content $EnvFile -Encoding UTF8 | ForEach-Object { + if ($_ -match '^\s*(WEB_PORT|ELECTRON_PORT|BACKEND_PORT)\s*=\s*(\d+)') { $ports[$matches[1]] = $matches[2] } +} +$w, $e, $b = $ports.WEB_PORT, $ports.ELECTRON_PORT, $ports.BACKEND_PORT + +$deployJs = @" +window.SOON_DEPLOY_CONFIG = { + api_v1_base: 'http://localhost:$b/api/v1', + api_admin_base: 'http://localhost:$b/api/admin', + front_base_url: 'http://localhost:$w', + web_base: 'http://localhost:$w', + version: 'local-dev' +}; +"@ + +$localPhp = @" + [ + 'env' => 'development', + 'debug' => true, + 'timezone' => 'Asia/Shanghai', + 'jwt_secret' => 'local-dev-jwt-secret-minimum-32-chars', + 'jwt_ttl' => 3600, + 'jwt_refresh_ttl' => 2592000, + 'cors_origins' => [ + 'http://localhost:$w', 'http://127.0.0.1:$w', + 'http://localhost:$e', 'http://127.0.0.1:$e', + ], + ], + 'db' => [ + 'host' => 'mysql', 'port' => 3306, 'name' => 'soondesign', + 'user' => 'soondesign', 'pass' => 'soondesign', 'charset' => 'utf8mb4', + ], + 'site' => [ + 'base_url' => 'http://localhost:$b', + 'front_base_url' => 'http://localhost:$w', + ], + 'storage' => [ + 'users_dir' => '/var/www/storage/users', + 'models_dir' => '/var/soonModels', + ], + 'limits' => ['free_quota_mb' => 20, 'preview_require_membership' => false], + 'rate_limits' => [ + 'admin/*' => ['capacity' => 120, 'window' => 60], + 'POST /api/v1/auth/login' => ['capacity' => 10, 'window' => 60], + 'POST /api/v1/auth/register' => ['capacity' => 5, 'window' => 60], + 'POST /api/v1/files' => ['capacity' => 30, 'window' => 60], + ], + 'alipay' => ['app_id' => '', 'private_key' => '', 'public_key' => '', 'sandbox' => true], + 'wechat' => [ + 'app_id' => '', 'mch_id' => '', 'mch_serial_no' => '', + 'mch_private_key' => '', 'api_v3_key' => '', 'sandbox' => true, + ], +]; +"@ + +$dir = Join-Path $PSScriptRoot 'config' +New-Item -ItemType Directory -Force -Path $dir | Out-Null +$enc = [Text.UTF8Encoding]::new($false) +[IO.File]::WriteAllText((Join-Path $dir 'deploy-config.js'), $deployJs.Trim() + "`n", $enc) +[IO.File]::WriteAllText((Join-Path $dir 'local.php'), $localPhp.Trim() + "`n", $enc) + +$webDeploy = Join-Path $Root 'frontend-web\assets\deploy-config.js' +[IO.File]::WriteAllText($webDeploy, $deployJs.Trim() + "`n", $enc) diff --git a/docs/API-PAGINATION.md b/docs/API-PAGINATION.md new file mode 100644 index 0000000..cb4a8f5 --- /dev/null +++ b/docs/API-PAGINATION.md @@ -0,0 +1,201 @@ +# 列表分页 API 契约 + +本文描述 `backend-web` 公开端(`/api/v1`)与管理端(`/api/admin`)列表接口的分页约定。实现以仓库内 PHP 控制器为准。 + +## 通用响应包装 + +所有 JSON 接口统一: + +```json +{ + "ok": true, + "data": { } +} +``` + +失败时 `ok: false`,含 `error`(机器码)与 `message`(人类可读文案)。 + +## 推荐分页形态(page / size / total) + +**新接口与前端管理列表优先使用此形态。** + +### 请求 Query + +| 参数 | 类型 | 默认 | 上限 | 说明 | +|------|------|------|------|------| +| `page` | int | `1` | — | 从 1 开始 | +| `size` | int | 见各接口 | 见各接口 | 每页条数 | + +### 响应 `data` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `items` | array | 当前页记录 | +| `total` | int | 符合条件的总条数(用于算总页数) | +| `page` | int | 当前页码(回显) | +| `size` | int | 当前每页条数(回显) | + +总页数:`ceil(total / size)`,`total === 0` 时视为 0 条、1 页。 + +### 示例 + +```http +GET /api/admin/users?page=2&size=20&q=test%40local.test +Authorization: Bearer +``` + +```json +{ + "ok": true, + "data": { + "items": [ { "id": 1, "email": "..." } ], + "total": 42, + "page": 2, + "size": 20 + } +} +``` + +--- + +## 管理端分页列表(`/api/admin`) + +需管理员 JWT(`role=admin`)。默认 `size` 与上限见下表。 + +| 方法 | 路径 | 默认 size | size 上限 | 额外筛选 Query | +|------|------|-----------|-----------|----------------| +| GET | `/api/admin/users` | 20 | 200 | `q` 邮箱模糊 | +| GET | `/api/admin/orders` | 20 | 200 | `q` 订单号或邮箱;`status` pending/paid/cancelled/refunded;`refund_status` none/pending/approved/rejected;`channel` alipay/wechat;`from`/`to` 日期 YYYY-MM-DD | +| GET | `/api/admin/audits` | 50 | 200 | `q` 操作/目标/管理员邮箱;`action` 精确操作码;`from`/`to` 日期 | + +实现参考: + +- `backend-web/src/Admin/Controllers/UsersController.php` +- `backend-web/src/Admin/Controllers/OrdersController.php` +- `backend-web/src/Admin/Controllers/AuditsController.php` + +### 管理端全量列表(无分页) + +以下接口一次返回全部 `items`,数据量预期较小: + +| 路径 | 说明 | +|------|------| +| `GET /api/admin/plans` | 套餐配置 | +| `GET /api/admin/settings` | 系统键值 | +| `GET /api/admin/payment/status` | PEM 文件元数据 | + +### 管理端固定条数嵌套列表 + +| 来源 | 条数 | 说明 | +|------|------|------| +| `GET /api/admin/stats` → `recent_orders` | 5 | 仪表盘最近订单,非翻页接口 | +| `GET /api/admin/users/{id}` → `recent_orders` | 5 | 用户详情内嵌 | + +--- + +## 公开端分页列表(`/api/v1`) + +需用户 JWT(除 plans 列表可匿名,见各控制器)。 + +| 方法 | 路径 | 默认 size | size 上限 | 分页参数 | 响应 | +|------|------|-----------|-----------|----------|------| +| GET | `/api/v1/files` | 50(legacy) | 200 | **推荐** `page`+`size`;**兼容** `limit`+`offset` | 见下文 | +| GET | `/api/v1/pay/orders` | 8 | 50 | `page`+`size` | page 形态 | + +实现参考: + +- `backend-web/src/Controllers/FileController.php` +- `backend-web/src/Controllers/PayController.php` +- `backend-web/src/Services/MembershipService.php`(`listOrders`) + +### `GET /api/v1/files` 双模式 + +**推荐(page 形态)** — 传 `page`≥1 时生效: + +```http +GET /api/v1/files?page=1&size=12 +``` + +```json +{ + "ok": true, + "data": { + "items": [ { "id": 1, "name": "design.soon", "size": 1024, "version": 1 } ], + "total": 25, + "page": 1, + "size": 12 + } +} +``` + +**兼容(offset 形态)** — 未传 `page` 或 `page=0` 时: + +```http +GET /api/v1/files?limit=50&offset=0 +``` + +```json +{ + "ok": true, + "data": { + "items": [ ], + "total": 25, + "limit": 50, + "offset": 0 + } +} +``` + +两种形态均含 `total`。新前端(首页文件列表、管理端)应使用 `page`+`size`。 + +### `GET /api/v1/pay/orders` + +会员中心订单历史,默认每页 8 条: + +```http +GET /api/v1/pay/orders?page=1&size=8 +``` + +### 公开端全量 / 非标准列表 + +| 路径 | 形态 | +|------|------| +| `GET /api/v1/plans` | `{ items: [...] }` 全量活跃套餐 | +| `GET /api/v1/plans/me` | 会员信息 + `recent_orders`(默认 8 条,非翻页) | +| `GET /api/v1/settings` | 扁平 key-value 对象,非 `items` 数组 | +| `GET /api/v1/soon-models` | manifest 内容 | + +--- + +## 前端约定 + +### 管理后台(`frontend-web/pages/admin`) + +- 分页状态:`AdminState`(`users` / `orders` / `audits` 的 `page`、`size` 及筛选字段) +- 分页 UI:`AdminPager` + `AdminUi.pagerHtml` / `bindPager` +- 请求:相对 admin base 的路径,如 `users?page=1&size=20` + +### 门户 / 会员 / 首页 + +| 页面 | 接口 | 默认 size | +|------|------|-----------| +| 会员订单 | `GET /api/v1/pay/orders` | 8 | +| 首页云端文件 | `GET /api/v1/files?page=&size=` | 12 | + +--- + +## 新增列表接口检查清单 + +1. 响应是否包含 `items` + `total`? +2. 是否使用 `page`(从 1)+ `size`,并在 `data` 中回显? +3. `size` 是否有合理默认值与上限(建议上限 200,管理端;用户订单建议 50)? +4. 筛选参数是否与分页独立(翻页时保留筛选)? +5. 是否在本文档「管理端/公开端」表中登记? + +--- + +## 变更记录 + +| 日期 | 说明 | +|------|------| +| 2026-06-08 | 初版:统一 page/size/total;files 兼容 limit/offset;pay/orders 分页 | diff --git a/docs/PAYMENT.md b/docs/PAYMENT.md new file mode 100644 index 0000000..3ec392e --- /dev/null +++ b/docs/PAYMENT.md @@ -0,0 +1,22 @@ +# 支付联调 + +## 密钥文件(Admin → 支付密钥 或 `backend-web/storage/payment/`) + +| 文件 | 用途 | +|------|------| +| alipay_private_key.pem | 支付宝商户私钥 | +| alipay_public_key.pem | 支付宝公钥 | +| wechat_mch_private_key.pem | 微信商户私钥 | +| wechat_api_v3_key.pem | APIv3 密钥(32 字节) | +| wechat_platform_cert.pem | 微信平台证书 | + +## local.php 必填项 + +`site.base_url`(HTTPS 外网)、`alipay.app_id`、`wechat.mch_id`、`wechat.mch_serial_no` + +## notify 路径 + +- `{site.base_url}/api/v1/pay/alipay/notify` +- `{site.base_url}/api/v1/pay/wechat/notify` + +本地开发需 ngrok 等穿透;重复 notify 应幂等返回 success。 diff --git a/docs/网页端功能清单.md b/docs/网页端功能清单.md deleted file mode 100644 index ca41258..0000000 --- a/docs/网页端功能清单.md +++ /dev/null @@ -1,83 +0,0 @@ -# SoonDesign 网页端功能清单 - -本文档列出 SoonDesign 各功能在**桌面端(Electron)**与**网页端**的实现情况,以及网页端不可用时的替代方案。 - ---- - -## 一、首页(index) - -| 功能 | 桌面端 | 网页端 | 替代/说明 | -|------|--------|--------|----------| -| 打开文件 | ✅ 系统对话框选文件,打开设计页 | ✅ 使用 `` 选文件,项目 JSON 存入 `sessionStorage`,通过 `soondesign_session:文件名` 传给设计页 | 网页端选文件后在新页打开,关闭后需重新选择 | -| 新建 1(单面) | ✅ 通过 ipcRenderer 打开设计页 | ✅ 通过 `platformBridge.openDesignPage("", 1)` 跳转 design1.web.html | 一致 | -| 新建 2(双面) | ✅ 通过 ipcRenderer 打开设计页 | ✅ 通过 `platformBridge.openDesignPage("", 2)` 跳转 design2.web.html | 一致 | -| 历史记录 | ✅ 从本地文件读取并展示 | ✅ 使用 `platformBridge.readHistory()`(localStorage) | 网页端历史仅限当前域名,且为路径/名称列表,点击打开需支持 sessionKey | -| 语言切换 | ✅ 正常 | ✅ 使用 localStorage 存语言,无系统 API 时沿用本地存储 | 一致 | -| 关于 | ✅ 正常 | ✅ 可显示版本等信息(若未注入 sysAPI 则仅显示界面) | 一致 | -| 关闭 | ✅ 通过 ipcRenderer 关闭窗口 | ⚠️ 使用 `window.close()` 或隐藏;部分浏览器限制脚本关闭页签 | 替代:用户手动关闭页签 | - ---- - -## 二、设计页(design1 / design2 通用) - -### 2.1 文件操作 - -| 功能 | 桌面端 | 网页端 | 替代/说明 | -|------|--------|--------|----------| -| 打开项目 | ✅ `fs.readFileSync` 读 .soon 后加载画布 | ✅ `platformBridge.readJsonFile(file)` 支持路径或 `soondesign_session:xxx`,异步加载 | 从首页“打开文件”传入的 sessionKey 在设计页通过 readJsonFile 从 sessionStorage 读取 | -| 保存 | ✅ `dialog.showSaveDialog` + `fs.writeFileSync` | ✅ 使用 `platformBridge.writeFile`(fileHandle 或默认文件名)+ 异步写入 | 网页端无 fileHandle 时触发下载 | -| 另存为 | ✅ 同上 | ✅ 同上,通过 showSaveDialog 选保存位置或文件名 | 一致 | -| 导出 PNG | ✅ 对话框选路径 + fs 写入 | ✅ Blob + `fileHandle.createWritable()` 或 `` + `URL.createObjectURL(blob)` 下载 | 一致 | -| 导出 PDF(仅 design2) | ✅ `dialog.showSaveDialog` + `fs.writeFile` | ✅ 使用桥 `showSaveDialog` + `writeFile(Blob)`,或 Blob 下载 | 一致 | -| 写历史(saveHistory) | ✅ fs 读写本地历史文件 | ✅ `platformBridge.readHistory` / `writeHistory`(localStorage) | 一致 | - -### 2.2 预览与打印 - -| 功能 | 桌面端 | 网页端 | 替代/说明 | -|------|--------|--------|----------| -| 预览 | ✅ 生成临时文件 + printJS 等 | ✅ 使用画布导出 Blob → base64 → Uint8Array → Blob URL 显示预览图 | 一致 | -| 打印 | ✅ 同上,调用打印 | ✅ 使用 `platformBridge.printPdf(blob)` 或 Blob URL 交给浏览器打印 | 一致(依赖桥提供 printPdf) | - -### 2.3 界面与编辑 - -| 功能 | 桌面端 | 网页端 | 替代/说明 | -|------|--------|--------|----------| -| 帮助 | ✅ 打开本地或在线帮助 | ✅ 新窗口打开帮助链接或 PDF | 一致 | -| 返回首页 | ✅ ipcRenderer 跳转首页 | ✅ `location.href` 跳转 index.web.html | 一致 | -| 字体 / 剪贴板 / 撤销重做 | ✅ 不依赖 Node | ✅ 同桌面端 | 一致 | -| 关闭前提示(未保存) | ✅ 通过 IPC 或 beforeunload | ✅ beforeunload 提示 | 一致 | - ---- - -## 三、design2 额外说明 - -- **保存 / 另存为**:design2 的 `output.js`、`ui.js` 已统一为:有 `platformBridge.writeFile` 时异步写入,否则使用 `fs.writeFileSync`,网页端走桥或下载。 -- **导出 PDF**:`design2/core.js` 的 `savePdf` 已接桥:网页端使用 `showSaveDialog` + `writeFile(blob)` 或 Blob 下载;预览/打印在无 fs 时使用 data URL,打印可走 `platformBridge.printPdf(pdfBlob)`。 -- **打开项目**:design2 已支持 `platformBridge.readJsonFile` 与 `openFile(file, jAlready)`;`ui.js` 打开对话框在网页端使用 `showOpenDialog` 与 `readJsonFile(file)` 后调用 `openFile(name, j)`。 - ---- - -## 四、平台桥接口(网页端 lib/platform/web.js) - -网页端通过 `window.platformBridge` 提供: - -- `openDesignPage(file, type)`:跳转设计页(file 可为空或 sessionKey) -- `openFirstPage()`:跳转首页 -- `openHelp()`:新窗口打开帮助 -- `runClose()`:尝试关闭当前页(受浏览器限制) -- `onClose(callback)`:beforeunload 时回调(关闭前确认) -- `readJsonFile(pathOrHandle)`:支持 `soondesign_session:xxx` 从 sessionStorage 读 JSON -- `readHistory` / `writeHistory`:历史记录(localStorage) -- `showOpenDialog` / `showSaveDialog`:文件选择(input 或 File System Access) -- `writeFile(content, fileHandleOrFilename)`:写字符串或 Blob -- `printPdf(blob)`:打印 PDF Blob(若未实现则可不提供) - -网页端 `ipcRenderer` 桩会响应:`get-sys-language`、`open-first-page`、`open-design-page`、`open-help-file`、`run-close`,并转发到上述桥方法。 - ---- - -## 五、总结 - -- **首页**:打开、新建、历史、语言、关于在网页端均可用;关闭为浏览器限制,无替代实现则用户手动关页。 -- **设计页**:打开(含 sessionKey)、保存、另存为、导出 PNG、预览、打印、历史、帮助、返回首页、编辑相关功能在网页端可用;design2 导出 PDF 建议后续接桥或改为下载。 -- **无法在网页实现的**:无;仅“关闭窗口”受浏览器限制,其余均有实现或替代方案。 diff --git a/public/css/design.css b/frontend-electron/assets/css/design.css similarity index 89% rename from public/css/design.css rename to frontend-electron/assets/css/design.css index d605543..a6c2e3e 100644 --- a/public/css/design.css +++ b/frontend-electron/assets/css/design.css @@ -1,419 +1,400 @@ - -html,body{ - background-color: #31373D; - color: #ffffff; - user-select: none; - height: 100%; - overflow: hidden; -} -img { - -webkit-user-drag: none; -} -.main{ - padding: 0 !important; - height: 100%; -} -.col-left{ - margin-right: 320px; -} -.col-right{ - float: right; - height: 100%; - width: 320px; -} -.tools-bar{ - height: 50px; - width: 100%; - border-bottom: 1px solid #000000; -} -.left-bar{ - border-right: 1px solid #000000; -} -.left-bar .tool{ - margin-left: 10px; - display: inline-block; - width: 32px; - height: 32px; - margin-top: 9px; -} -.left-bar .tool:hover{ - background-color: #646E76; -} -.left-bar .tool img{ - height: 24px; - width: 24px; - margin-top: 4px; - margin-left: 4px; -} -.right-bar .tool{ - margin-left: 14px; - display: inline-block; - width: 32px; - height: 32px; - margin-top: 9px; -} -.right-bar .tool:hover{ - background-color: #646E76; -} -.right-bar .tool img{ - height: 24px; - width: 24px; - margin-top: 4px; - margin-left: 4px; -} -.mode1{ - display: inline-block; - border-right: 1px solid #000000; - padding-right: 10px; - height: 50px; -} -.mode2{ - - display: inline-block; - height: 50px; - padding-right: 10px; - border-right: 1px solid #000000; -} -.mode3{ - float: right; - line-height: 50px; - margin-right: 30px; -} -.white{ - color: white; -} -.white:hover{ - color: white !important; -} - -.container{ - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.container .left{ - position: absolute; - top: 50px; - bottom: 0px; - width: 50px; - border-right: 1px solid #000000; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - display: flex; - flex-direction: column; - align-items: center; -} - -.container .right{ - margin-left: 50px; - border-right: 1px solid #000000; - height: 100%; -} -#canvas-div{ - position: relative; -} - -.canvas_box { - position: relative; - z-index: 4; - display: flex; - align-items: center; - justify-content: start; -} -.canvas_bg { - position: absolute; - top: 0; - left: 0; - z-index: 1; - opacity: 0; - width: 100%; - height: 100%; -} -.canvas_bg_img { - max-width: calc(100% - 470px); - max-height: calc(100% - 180px); -} -.container .tool{ - width: 32px; - height: 32px; - margin-top: 15px; - display: flex; - justify-content: center; - align-items: center; -} -.container .tool img{ - width: 24px; - height: 24px; -} -.container .tool:hover{ - background-color: #646E76; -} - -.layui-form-label{ - width: 45px; - padding-left: 5px; -} - -.layui-input-block { - margin-left: 70px; - -} - -.component-title{ - height: 32px; - line-height: 32px; - width: 100%; - font-size: 18px; - background-color: #4E575F; - text-align: center; - padding-left: 10px; - box-sizing: border-box; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; -} -.component-line{ - font-size: 14px; - padding: 5px; -} -.component-line span{ - color: #969FA7; -} -.component-line .tool { - cursor: pointer; - padding: 4px; -} -.component-line .tool:hover{ - background-color: #646E76; -} -.component-line .tool1 { - background-color: #646E76; - padding: 4px; -} -.ui-input{ - background-color: #31373D; - border: #646E76 solid 1px; - color: #ffffff; - font-size: 16px; -} -.ui-input:hover{ - background-color: #646E76; -} -textarea{ - background-color: #31373D; - border: #646E76 solid 1px; - color: #ffffff; - font-size: 16px; -} -textarea:hover{ - background-color: #646E76; -} -.control{ - display: none; -} -#base_control{ - display: none; -} -.pic_bg{ - display: inline-block; - background-color: #31373D; - border: #646E76 solid 1px; - padding: 3px 10px; - color: #ffffff; - font-size: 16px; - position: relative; - margin-left: 5px; - cursor: pointer; - border-radius: 6px; -} -.pic_bg:hover{ - background-color: #646E76; -} - - -.input_color{ - position: relative; - top:5px; -} -.input_color[type='color']::-webkit-color-swatch-wrapper{padding: 0;} -.input_color[type='color']::-webkit-color-swatch{border: 0;} - -.radio{ - position: relative; - top:2px; - left:3px -} - -.ui-select{ - background-color: #31373D; - border: #646E76 solid 1px; - color: #ffffff; - font-size: 16px; -} - - -::-webkit-scrollbar -{ - width: 8px; - height: 8px; - background-color: #31373D; -} - -/*定义滚动条轨道 内阴影+圆角*/ -::-webkit-scrollbar-track -{ - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); - border-radius: 10px; - background-color: #F5F5F5; -} - -/*定义滑块 内阴影+圆角*/ -::-webkit-scrollbar-thumb -{ - border-radius: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: #555; -} - -.ui-button{ - display: inline-block; - background-color: #31373D; - border: #646E76 solid 1px; - padding: 3px 10px; - color: #ffffff; - font-size: 16px; - position: relative; - margin-left: 5px; - cursor: pointer; - border-radius: 6px; -} -.ui-button:hover{ - background-color: #646E76; -} - -.ui-button-active{ - background-color: #646E76; -} -.ui-radio{ - -} -.ui-radio-acitve{ - background-color: #646E76; -} -.ui-radio:hover{ - background-color: #646E76; -} -.obj_list{ - overflow-y: auto; - -} -.obj_list div{ - height: 32px; - line-height: 32px; - width: 100%; - font-size: 14px; - padding-left: 10px; - box-sizing: border-box; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - overflow: hidden; -} -.obj_list img{ - position: relative; - right: 4px; - width: 24px; -} -.obj_list div:hover{ - background-color: #6F7A84; -} - -#posText{ - line-height: 50px; - font-size: 18px; - margin-left: 10px; -} - -#rotate:hover{ - background-color: #646E76; -} - -#object_attribute{ - height: 470px; - overflow-y: auto; -} - -.transparent-button{ - display: inline-block; - height: 19px; - border: #646E76 solid 1px; - position: relative; - bottom: 2px; - padding: 3px; -} -.transparent-button:hover{ - background-color: #646E76; -} - - -.mode-left{ - display: inline-block; -} -.mode-warp{ - display: flex; - height: 50px; - align-items: center; -} -.mode-left .img-warp{ - height: 32px; - width: 32px; - display: flex; - justify-content: center; - align-items: center; - margin-left: 10px; -} - -.mode-left .img-warp:hover{ - background-color: #646E76; -} - -.mode-left .img-warp1 { - width: 0; - height: 0; - overflow: hidden; - margin: 0; - padding: 0; - opacity: 0; - border: none; -} - - -.line{ - height: 50px; - width: 1px; - background-color: #000; - margin-left: 10px; -} - -.rotate-div{ - position: relative; - transition: 200ms; -} -.rotate-wrap{ - position: absolute; - top:0px; - left:0px; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,.3); - visibility: hidden; - border-radius: 3%; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; -} - -.rotate-div:hover .rotate-wrap{ - visibility: visible; + +html,body{ + background-color: #31373D; + color: #ffffff; + user-select: none; + height: 100%; + overflow: hidden; +} +img { + -webkit-user-drag: none; +} +.main{ + padding: 0 !important; + height: 100%; +} +.col-left{ + margin-right: 320px; +} +.col-right{ + float: right; + height: 100%; + width: 320px; +} +.tools-bar{ + height: 50px; + width: 100%; + border-bottom: 1px solid #000000; +} +.left-bar{ + border-right: 1px solid #000000; +} +.left-bar .tool{ + margin-left: 10px; + display: inline-block; + width: 32px; + height: 32px; + margin-top: 9px; +} +.left-bar .tool:hover{ + background-color: #646E76; +} +.left-bar .tool img{ + height: 24px; + width: 24px; + margin-top: 4px; + margin-left: 4px; +} +.right-bar .tool{ + margin-left: 14px; + display: inline-block; + width: 32px; + height: 32px; + margin-top: 9px; +} +.right-bar .tool:hover{ + background-color: #646E76; +} +.right-bar .tool img{ + height: 24px; + width: 24px; + margin-top: 4px; + margin-left: 4px; +} +.mode1{ + display: inline-block; + border-right: 1px solid #000000; + padding-right: 10px; + height: 50px; +} +.mode2{ + + display: inline-block; + height: 50px; + padding-right: 10px; + border-right: 1px solid #000000; +} +.mode3{ + float: right; + line-height: 50px; + margin-right: 30px; +} +.white{ + color: white; +} +.white:hover{ + color: white !important; +} + +.container{ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.container .left{ + position: absolute; + top: 50px; + bottom: 0px; + width: 50px; + border-right: 1px solid #000000; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; +} + +.container .right{ + margin-left: 50px; + border-right: 1px solid #000000; + height: 100%; +} +#canvas-div{ + position: relative; +} + +.canvas_box { + position: relative; + z-index: 4; + display: flex; + align-items: center; + justify-content: start; +} +.canvas_bg { + position: absolute; + top: 0; + left: 0; + z-index: 1; + opacity: 0; + width: 100%; + height: 100%; +} +.canvas_bg_img { + max-width: calc(100% - 470px); + max-height: calc(100% - 180px); +} +.container .tool{ + width: 32px; + height: 32px; + margin-top: 15px; + display: flex; + justify-content: center; + align-items: center; +} +.container .tool img{ + width: 24px; + height: 24px; +} +.container .tool:hover{ + background-color: #646E76; +} + +.layui-form-label{ + width: 45px; + padding-left: 5px; +} + +.layui-input-block { + margin-left: 70px; + +} + +.component-title{ + height: 32px; + line-height: 32px; + width: 100%; + font-size: 18px; + background-color: #4E575F; + text-align: center; + padding-left: 10px; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; +} +.component-line{ + font-size: 14px; + padding: 5px; +} +.component-line span{ + color: #969FA7; +} +.component-line .tool { + cursor: pointer; + padding: 4px; +} +.component-line .tool:hover{ + background-color: #646E76; +} +.component-line .tool1 { + background-color: #646E76; + padding: 4px; +} +.ui-input{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; +} +.ui-input:hover{ + background-color: #646E76; +} +textarea{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; +} +textarea:hover{ + background-color: #646E76; +} +.control{ + display: none; +} +#base_control{ + display: none; +} +.pic_bg{ + display: inline-block; + background-color: #31373D; + border: #646E76 solid 1px; + padding: 3px 10px; + color: #ffffff; + font-size: 16px; + position: relative; + margin-left: 5px; + cursor: pointer; + border-radius: 6px; +} +.pic_bg:hover{ + background-color: #646E76; +} + + +.input_color{ + position: relative; + top:5px; +} +.input_color[type='color']::-webkit-color-swatch-wrapper{padding: 0;} +.input_color[type='color']::-webkit-color-swatch{border: 0;} + +.radio{ + position: relative; + top:2px; + left:3px +} + +.ui-select{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; +} + + +::-webkit-scrollbar +{ + width: 8px; + height: 8px; + background-color: #31373D; +} + +/*定义滚动条轨道 内阴影+圆角*/ +::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px; + background-color: #F5F5F5; +} + +/*定义滑块 内阴影+圆角*/ +::-webkit-scrollbar-thumb +{ + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; +} + +.ui-button{ + display: inline-block; + background-color: #31373D; + border: #646E76 solid 1px; + padding: 3px 10px; + color: #ffffff; + font-size: 16px; + position: relative; + margin-left: 5px; + cursor: pointer; + border-radius: 6px; +} +.ui-button:hover{ + background-color: #646E76; +} + +.ui-button-active{ + background-color: #646E76; +} +.ui-radio{ + +} +.ui-radio-acitve{ + background-color: #646E76; +} +.ui-radio:hover{ + background-color: #646E76; +} +.obj_list{ + overflow-y: auto; + overflow-x: hidden; +} + +#posText{ + line-height: 50px; + font-size: 18px; + margin-left: 10px; +} + +#rotate:hover{ + background-color: #646E76; +} + +#object_attribute{ + height: 470px; + overflow-y: auto; +} + +.transparent-button{ + display: inline-block; + height: 19px; + border: #646E76 solid 1px; + position: relative; + bottom: 2px; + padding: 3px; +} +.transparent-button:hover{ + background-color: #646E76; +} + + +.mode-left{ + display: inline-block; +} +.mode-warp{ + display: flex; + height: 50px; + align-items: center; +} +.mode-left .img-warp{ + height: 32px; + width: 32px; + display: flex; + justify-content: center; + align-items: center; + margin-left: 10px; +} + +.mode-left .img-warp:hover{ + background-color: #646E76; +} + +.mode-left .img-warp1 { + width: 0; + height: 0; + overflow: hidden; + margin: 0; + padding: 0; + opacity: 0; + border: none; +} + + +.line{ + height: 50px; + width: 1px; + background-color: #000; + margin-left: 10px; +} + +.rotate-div{ + position: relative; + transition: 200ms; +} +.rotate-wrap{ + position: absolute; + top:0px; + left:0px; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,.3); + visibility: hidden; + border-radius: 3%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.rotate-div:hover .rotate-wrap{ + visibility: visible; } \ No newline at end of file diff --git a/public/css/index.css b/frontend-electron/assets/css/index.css similarity index 94% rename from public/css/index.css rename to frontend-electron/assets/css/index.css index 33b601d..aeed987 100644 --- a/public/css/index.css +++ b/frontend-electron/assets/css/index.css @@ -1,125 +1,125 @@ -html,body{ - background-color: #31373D; - color: #ffffff; - user-select: none; - height: 100%; - overflow: hidden; -} - -img { - -webkit-user-drag: none; -} -.main{ - margin-top: 40px; - position: relative; - height: 100%; -} -.main .left{ - padding-right: 40px; - padding-left: 40px; - border-right: 1px solid #000000; - height: 100%; -} -.left h1{ - position: relative; -} -.left .card{ - background-color: #3E454D; - width: 100%; - /* height: 130px; */ - margin-top: 30px; - color: #999999; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} -.left .card:hover{ - background-color: #646E76; - color: #ffffff !important; -} -.card .rect{ - padding: 10px; - text-align: center; -} -.rect img{ - max-width: 70%; - max-height: 70%; -} -.rect1 img { - border-radius: 0; -} -.rect .tip{ - margin-top: 10px; - font-size: 16px; - letter-spacing:1px; - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.right{ - padding-left: 30px; - padding-right: 30px; - height: calc(100vh - 40px); - position: relative; -} -.right .recent{ - position: absolute; - left: 50%; - transform: translate(-50%, 0); - line-height: 40px; - font-size: 2em; -} -.right .card{ - background-color: #3E454D; - width: 230px; - height: 200px; - margin-top: 30px; - color: #999999; - cursor: pointer; - display: inline-block; - margin-right: 40px; - display: flex; - align-items: center; - justify-content: center; -} -.ui-select{ - background-color: #31373D; - border: #646E76 solid 1px; - color: #ffffff; - font-size: 16px; - position: absolute; - bottom: 50px; -} -.card-list{ - height: calc(100% - 78px); - display: flex; - flex-wrap: wrap; - overflow-y: auto; - align-content: flex-start; -} - - -::-webkit-scrollbar -{ - width: 8px; - height: 8px; - background-color: #31373D; -} - -/*定义滚动条轨道 内阴影+圆角*/ -::-webkit-scrollbar-track -{ - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); - border-radius: 10px; - background-color: #F5F5F5; -} - -/*定义滑块 内阴影+圆角*/ -::-webkit-scrollbar-thumb -{ - border-radius: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: #555; -} +html,body{ + background-color: #31373D; + color: #ffffff; + user-select: none; + height: 100%; + overflow: hidden; +} + +img { + -webkit-user-drag: none; +} +.main{ + margin-top: 40px; + position: relative; + height: 100%; +} +.main .left{ + padding-right: 40px; + padding-left: 40px; + border-right: 1px solid #000000; + height: 100%; +} +.left h1{ + position: relative; +} +.left .card{ + background-color: #3E454D; + width: 100%; + /* height: 130px; */ + margin-top: 30px; + color: #999999; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} +.left .card:hover{ + background-color: #646E76; + color: #ffffff !important; +} +.card .rect{ + padding: 10px; + text-align: center; +} +.rect img{ + max-width: 70%; + max-height: 70%; +} +.rect1 img { + border-radius: 0; +} +.rect .tip{ + margin-top: 10px; + font-size: 16px; + letter-spacing:1px; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.right{ + padding-left: 30px; + padding-right: 30px; + height: calc(100vh - 40px); + position: relative; +} +.right .recent{ + position: absolute; + left: 50%; + transform: translate(-50%, 0); + line-height: 40px; + font-size: 2em; +} +.right .card{ + background-color: #3E454D; + width: 230px; + height: 200px; + margin-top: 30px; + color: #999999; + cursor: pointer; + display: inline-block; + margin-right: 40px; + display: flex; + align-items: center; + justify-content: center; +} +.ui-select{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; + position: absolute; + bottom: 50px; +} +.card-list{ + height: calc(100% - 78px); + display: flex; + flex-wrap: wrap; + overflow-y: auto; + align-content: flex-start; +} + + +::-webkit-scrollbar +{ + width: 8px; + height: 8px; + background-color: #31373D; +} + +/*定义滚动条轨道 内阴影+圆角*/ +::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px; + background-color: #F5F5F5; +} + +/*定义滑块 内阴影+圆角*/ +::-webkit-scrollbar-thumb +{ + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; +} diff --git a/help/User Manual.pdf b/frontend-electron/assets/help/User Manual.pdf similarity index 100% rename from help/User Manual.pdf rename to frontend-electron/assets/help/User Manual.pdf diff --git a/public/images/IconA.png b/frontend-electron/assets/images/IconA.png similarity index 100% rename from public/images/IconA.png rename to frontend-electron/assets/images/IconA.png diff --git a/public/images/IconA256.ico b/frontend-electron/assets/images/IconA256.ico similarity index 100% rename from public/images/IconA256.ico rename to frontend-electron/assets/images/IconA256.ico diff --git a/public/images/IconA256.png b/frontend-electron/assets/images/IconA256.png similarity index 100% rename from public/images/IconA256.png rename to frontend-electron/assets/images/IconA256.png diff --git a/public/images/IconA64.png b/frontend-electron/assets/images/IconA64.png similarity index 100% rename from public/images/IconA64.png rename to frontend-electron/assets/images/IconA64.png diff --git a/public/images/about24.png b/frontend-electron/assets/images/about24.png similarity index 100% rename from public/images/about24.png rename to frontend-electron/assets/images/about24.png diff --git a/public/images/back_bg_1.png b/frontend-electron/assets/images/back_bg_1.png similarity index 100% rename from public/images/back_bg_1.png rename to frontend-electron/assets/images/back_bg_1.png diff --git a/public/images/back_bg_2.png b/frontend-electron/assets/images/back_bg_2.png similarity index 100% rename from public/images/back_bg_2.png rename to frontend-electron/assets/images/back_bg_2.png diff --git a/public/images/back_bg_bak_1.png b/frontend-electron/assets/images/back_bg_bak_1.png similarity index 100% rename from public/images/back_bg_bak_1.png rename to frontend-electron/assets/images/back_bg_bak_1.png diff --git a/public/images/back_bg_bak_2.png b/frontend-electron/assets/images/back_bg_bak_2.png similarity index 100% rename from public/images/back_bg_bak_2.png rename to frontend-electron/assets/images/back_bg_bak_2.png diff --git a/public/images/barcode.svg b/frontend-electron/assets/images/barcode.svg similarity index 100% rename from public/images/barcode.svg rename to frontend-electron/assets/images/barcode.svg diff --git a/public/images/barcode1.png b/frontend-electron/assets/images/barcode1.png similarity index 100% rename from public/images/barcode1.png rename to frontend-electron/assets/images/barcode1.png diff --git a/public/images/barcode32.png b/frontend-electron/assets/images/barcode32.png similarity index 100% rename from public/images/barcode32.png rename to frontend-electron/assets/images/barcode32.png diff --git a/public/images/bg_1.png b/frontend-electron/assets/images/bg_1.png similarity index 100% rename from public/images/bg_1.png rename to frontend-electron/assets/images/bg_1.png diff --git a/public/images/bg_2.png b/frontend-electron/assets/images/bg_2.png similarity index 100% rename from public/images/bg_2.png rename to frontend-electron/assets/images/bg_2.png diff --git a/public/images/bg_back.png b/frontend-electron/assets/images/bg_back.png similarity index 100% rename from public/images/bg_back.png rename to frontend-electron/assets/images/bg_back.png diff --git a/public/images/bg_back_1.png b/frontend-electron/assets/images/bg_back_1.png similarity index 100% rename from public/images/bg_back_1.png rename to frontend-electron/assets/images/bg_back_1.png diff --git a/public/images/bg_back_2.png b/frontend-electron/assets/images/bg_back_2.png similarity index 100% rename from public/images/bg_back_2.png rename to frontend-electron/assets/images/bg_back_2.png diff --git a/public/images/bg_back_bak_1.png b/frontend-electron/assets/images/bg_back_bak_1.png similarity index 100% rename from public/images/bg_back_bak_1.png rename to frontend-electron/assets/images/bg_back_bak_1.png diff --git a/public/images/bg_back_bak_2.png b/frontend-electron/assets/images/bg_back_bak_2.png similarity index 100% rename from public/images/bg_back_bak_2.png rename to frontend-electron/assets/images/bg_back_bak_2.png diff --git a/public/images/bg_bak_1.png b/frontend-electron/assets/images/bg_bak_1.png similarity index 100% rename from public/images/bg_bak_1.png rename to frontend-electron/assets/images/bg_bak_1.png diff --git a/public/images/bg_bak_2.png b/frontend-electron/assets/images/bg_bak_2.png similarity index 100% rename from public/images/bg_bak_2.png rename to frontend-electron/assets/images/bg_bak_2.png diff --git a/public/images/bg_front_1.png b/frontend-electron/assets/images/bg_front_1.png similarity index 100% rename from public/images/bg_front_1.png rename to frontend-electron/assets/images/bg_front_1.png diff --git a/public/images/bg_front_2.png b/frontend-electron/assets/images/bg_front_2.png similarity index 100% rename from public/images/bg_front_2.png rename to frontend-electron/assets/images/bg_front_2.png diff --git a/public/images/bg_front_bak_1.png b/frontend-electron/assets/images/bg_front_bak_1.png similarity index 100% rename from public/images/bg_front_bak_1.png rename to frontend-electron/assets/images/bg_front_bak_1.png diff --git a/public/images/bg_front_bak_2.png b/frontend-electron/assets/images/bg_front_bak_2.png similarity index 100% rename from public/images/bg_front_bak_2.png rename to frontend-electron/assets/images/bg_front_bak_2.png diff --git a/public/images/bold24.png b/frontend-electron/assets/images/bold24.png similarity index 100% rename from public/images/bold24.png rename to frontend-electron/assets/images/bold24.png diff --git a/public/images/bottom24.png b/frontend-electron/assets/images/bottom24.png similarity index 100% rename from public/images/bottom24.png rename to frontend-electron/assets/images/bottom24.png diff --git a/public/images/center_x24.png b/frontend-electron/assets/images/center_x24.png similarity index 100% rename from public/images/center_x24.png rename to frontend-electron/assets/images/center_x24.png diff --git a/public/images/center_y24.png b/frontend-electron/assets/images/center_y24.png similarity index 100% rename from public/images/center_y24.png rename to frontend-electron/assets/images/center_y24.png diff --git a/public/images/circle24.png b/frontend-electron/assets/images/circle24.png similarity index 100% rename from public/images/circle24.png rename to frontend-electron/assets/images/circle24.png diff --git a/public/images/circle32.png b/frontend-electron/assets/images/circle32.png similarity index 100% rename from public/images/circle32.png rename to frontend-electron/assets/images/circle32.png diff --git a/public/images/clean24.png b/frontend-electron/assets/images/clean24.png similarity index 100% rename from public/images/clean24.png rename to frontend-electron/assets/images/clean24.png diff --git a/public/images/copy24.png b/frontend-electron/assets/images/copy24.png similarity index 100% rename from public/images/copy24.png rename to frontend-electron/assets/images/copy24.png diff --git a/public/images/counter.svg b/frontend-electron/assets/images/counter.svg similarity index 100% rename from public/images/counter.svg rename to frontend-electron/assets/images/counter.svg diff --git a/public/images/counter32.png b/frontend-electron/assets/images/counter32.png similarity index 100% rename from public/images/counter32.png rename to frontend-electron/assets/images/counter32.png diff --git a/public/images/delete24.png b/frontend-electron/assets/images/delete24.png similarity index 100% rename from public/images/delete24.png rename to frontend-electron/assets/images/delete24.png diff --git a/public/images/f1_objects.png b/frontend-electron/assets/images/f1_objects.png similarity index 100% rename from public/images/f1_objects.png rename to frontend-electron/assets/images/f1_objects.png diff --git a/public/images/favicon.icns b/frontend-electron/assets/images/favicon.icns similarity index 100% rename from public/images/favicon.icns rename to frontend-electron/assets/images/favicon.icns diff --git a/public/images/favicon.ico b/frontend-electron/assets/images/favicon.ico similarity index 100% rename from public/images/favicon.ico rename to frontend-electron/assets/images/favicon.ico diff --git a/public/images/favicon.svg b/frontend-electron/assets/images/favicon.svg similarity index 100% rename from public/images/favicon.svg rename to frontend-electron/assets/images/favicon.svg diff --git a/public/images/front_bg1_1.png b/frontend-electron/assets/images/front_bg1_1.png similarity index 100% rename from public/images/front_bg1_1.png rename to frontend-electron/assets/images/front_bg1_1.png diff --git a/public/images/front_bg1_2.png b/frontend-electron/assets/images/front_bg1_2.png similarity index 100% rename from public/images/front_bg1_2.png rename to frontend-electron/assets/images/front_bg1_2.png diff --git a/public/images/front_bg2_1.png b/frontend-electron/assets/images/front_bg2_1.png similarity index 100% rename from public/images/front_bg2_1.png rename to frontend-electron/assets/images/front_bg2_1.png diff --git a/public/images/front_bg2_2.png b/frontend-electron/assets/images/front_bg2_2.png similarity index 100% rename from public/images/front_bg2_2.png rename to frontend-electron/assets/images/front_bg2_2.png diff --git a/public/images/front_bg_bak_1.png b/frontend-electron/assets/images/front_bg_bak_1.png similarity index 100% rename from public/images/front_bg_bak_1.png rename to frontend-electron/assets/images/front_bg_bak_1.png diff --git a/public/images/front_bg_bak_2.png b/frontend-electron/assets/images/front_bg_bak_2.png similarity index 100% rename from public/images/front_bg_bak_2.png rename to frontend-electron/assets/images/front_bg_bak_2.png diff --git a/public/images/help24.png b/frontend-electron/assets/images/help24.png similarity index 100% rename from public/images/help24.png rename to frontend-electron/assets/images/help24.png diff --git a/public/images/image24.png b/frontend-electron/assets/images/image24.png similarity index 100% rename from public/images/image24.png rename to frontend-electron/assets/images/image24.png diff --git a/public/images/image32.png b/frontend-electron/assets/images/image32.png similarity index 100% rename from public/images/image32.png rename to frontend-electron/assets/images/image32.png diff --git a/public/images/images.svg b/frontend-electron/assets/images/images.svg similarity index 100% rename from public/images/images.svg rename to frontend-electron/assets/images/images.svg diff --git a/public/images/images_add.svg b/frontend-electron/assets/images/images_add.svg similarity index 100% rename from public/images/images_add.svg rename to frontend-electron/assets/images/images_add.svg diff --git a/public/images/italics24.png b/frontend-electron/assets/images/italics24.png similarity index 100% rename from public/images/italics24.png rename to frontend-electron/assets/images/italics24.png diff --git a/public/images/label24.png b/frontend-electron/assets/images/label24.png similarity index 100% rename from public/images/label24.png rename to frontend-electron/assets/images/label24.png diff --git a/public/images/label32.png b/frontend-electron/assets/images/label32.png similarity index 100% rename from public/images/label32.png rename to frontend-electron/assets/images/label32.png diff --git a/public/images/left24.png b/frontend-electron/assets/images/left24.png similarity index 100% rename from public/images/left24.png rename to frontend-electron/assets/images/left24.png diff --git a/public/images/line.svg b/frontend-electron/assets/images/line.svg similarity index 100% rename from public/images/line.svg rename to frontend-electron/assets/images/line.svg diff --git a/public/images/line24.png b/frontend-electron/assets/images/line24.png similarity index 100% rename from public/images/line24.png rename to frontend-electron/assets/images/line24.png diff --git a/public/images/line32.png b/frontend-electron/assets/images/line32.png similarity index 100% rename from public/images/line32.png rename to frontend-electron/assets/images/line32.png diff --git a/public/images/linethrough24.png b/frontend-electron/assets/images/linethrough24.png similarity index 100% rename from public/images/linethrough24.png rename to frontend-electron/assets/images/linethrough24.png diff --git a/public/images/lock20.png b/frontend-electron/assets/images/lock20.png similarity index 100% rename from public/images/lock20.png rename to frontend-electron/assets/images/lock20.png diff --git a/public/images/logo.ico b/frontend-electron/assets/images/logo.ico similarity index 100% rename from public/images/logo.ico rename to frontend-electron/assets/images/logo.ico diff --git a/public/images/logo.png b/frontend-electron/assets/images/logo.png similarity index 100% rename from public/images/logo.png rename to frontend-electron/assets/images/logo.png diff --git a/public/images/logo256.ico b/frontend-electron/assets/images/logo256.ico similarity index 100% rename from public/images/logo256.ico rename to frontend-electron/assets/images/logo256.ico diff --git a/public/images/newfile.png b/frontend-electron/assets/images/newfile.png similarity index 100% rename from public/images/newfile.png rename to frontend-electron/assets/images/newfile.png diff --git a/public/images/newfile24.png b/frontend-electron/assets/images/newfile24.png similarity index 100% rename from public/images/newfile24.png rename to frontend-electron/assets/images/newfile24.png diff --git a/public/images/next.png b/frontend-electron/assets/images/next.png similarity index 100% rename from public/images/next.png rename to frontend-electron/assets/images/next.png diff --git a/public/images/next24.png b/frontend-electron/assets/images/next24.png similarity index 100% rename from public/images/next24.png rename to frontend-electron/assets/images/next24.png diff --git a/public/images/op_1.png b/frontend-electron/assets/images/op_1.png similarity index 100% rename from public/images/op_1.png rename to frontend-electron/assets/images/op_1.png diff --git a/public/images/op_2.png b/frontend-electron/assets/images/op_2.png similarity index 100% rename from public/images/op_2.png rename to frontend-electron/assets/images/op_2.png diff --git a/public/images/op_bak_1.png b/frontend-electron/assets/images/op_bak_1.png similarity index 100% rename from public/images/op_bak_1.png rename to frontend-electron/assets/images/op_bak_1.png diff --git a/public/images/op_bak_2.png b/frontend-electron/assets/images/op_bak_2.png similarity index 100% rename from public/images/op_bak_2.png rename to frontend-electron/assets/images/op_bak_2.png diff --git a/public/images/open.png b/frontend-electron/assets/images/open.png similarity index 100% rename from public/images/open.png rename to frontend-electron/assets/images/open.png diff --git a/public/images/open24.png b/frontend-electron/assets/images/open24.png similarity index 100% rename from public/images/open24.png rename to frontend-electron/assets/images/open24.png diff --git a/public/images/outimage24.png b/frontend-electron/assets/images/outimage24.png similarity index 100% rename from public/images/outimage24.png rename to frontend-electron/assets/images/outimage24.png diff --git a/public/images/outimage32.png b/frontend-electron/assets/images/outimage32.png similarity index 100% rename from public/images/outimage32.png rename to frontend-electron/assets/images/outimage32.png diff --git a/public/images/outpic200.png b/frontend-electron/assets/images/outpic200.png similarity index 100% rename from public/images/outpic200.png rename to frontend-electron/assets/images/outpic200.png diff --git a/public/images/outpic_extra.png b/frontend-electron/assets/images/outpic_extra.png similarity index 100% rename from public/images/outpic_extra.png rename to frontend-electron/assets/images/outpic_extra.png diff --git a/public/images/output.png b/frontend-electron/assets/images/output.png similarity index 100% rename from public/images/output.png rename to frontend-electron/assets/images/output.png diff --git a/public/images/output24.png b/frontend-electron/assets/images/output24.png similarity index 100% rename from public/images/output24.png rename to frontend-electron/assets/images/output24.png diff --git a/public/images/paste24.png b/frontend-electron/assets/images/paste24.png similarity index 100% rename from public/images/paste24.png rename to frontend-electron/assets/images/paste24.png diff --git a/public/images/previous.png b/frontend-electron/assets/images/previous.png similarity index 100% rename from public/images/previous.png rename to frontend-electron/assets/images/previous.png diff --git a/public/images/previous24.png b/frontend-electron/assets/images/previous24.png similarity index 100% rename from public/images/previous24.png rename to frontend-electron/assets/images/previous24.png diff --git a/public/images/qrcode.svg b/frontend-electron/assets/images/qrcode.svg similarity index 100% rename from public/images/qrcode.svg rename to frontend-electron/assets/images/qrcode.svg diff --git a/public/images/qrcode32.png b/frontend-electron/assets/images/qrcode32.png similarity index 100% rename from public/images/qrcode32.png rename to frontend-electron/assets/images/qrcode32.png diff --git a/public/images/qrcode400.png b/frontend-electron/assets/images/qrcode400.png similarity index 100% rename from public/images/qrcode400.png rename to frontend-electron/assets/images/qrcode400.png diff --git a/public/images/rect.svg b/frontend-electron/assets/images/rect.svg similarity index 100% rename from public/images/rect.svg rename to frontend-electron/assets/images/rect.svg diff --git a/public/images/rect24.png b/frontend-electron/assets/images/rect24.png similarity index 100% rename from public/images/rect24.png rename to frontend-electron/assets/images/rect24.png diff --git a/public/images/rect32.png b/frontend-electron/assets/images/rect32.png similarity index 100% rename from public/images/rect32.png rename to frontend-electron/assets/images/rect32.png diff --git a/public/images/right24.png b/frontend-electron/assets/images/right24.png similarity index 100% rename from public/images/right24.png rename to frontend-electron/assets/images/right24.png diff --git a/public/images/rotate.png b/frontend-electron/assets/images/rotate.png similarity index 100% rename from public/images/rotate.png rename to frontend-electron/assets/images/rotate.png diff --git a/public/images/rotate20.png b/frontend-electron/assets/images/rotate20.png similarity index 100% rename from public/images/rotate20.png rename to frontend-electron/assets/images/rotate20.png diff --git a/public/images/round.svg b/frontend-electron/assets/images/round.svg similarity index 100% rename from public/images/round.svg rename to frontend-electron/assets/images/round.svg diff --git a/public/images/save.png b/frontend-electron/assets/images/save.png similarity index 100% rename from public/images/save.png rename to frontend-electron/assets/images/save.png diff --git a/public/images/save24.png b/frontend-electron/assets/images/save24.png similarity index 100% rename from public/images/save24.png rename to frontend-electron/assets/images/save24.png diff --git a/public/images/text.svg b/frontend-electron/assets/images/text.svg similarity index 100% rename from public/images/text.svg rename to frontend-electron/assets/images/text.svg diff --git a/public/images/text2.svg b/frontend-electron/assets/images/text2.svg similarity index 100% rename from public/images/text2.svg rename to frontend-electron/assets/images/text2.svg diff --git a/public/images/text24.png b/frontend-electron/assets/images/text24.png similarity index 100% rename from public/images/text24.png rename to frontend-electron/assets/images/text24.png diff --git a/public/images/text3.svg b/frontend-electron/assets/images/text3.svg similarity index 98% rename from public/images/text3.svg rename to frontend-electron/assets/images/text3.svg index cd8300d..dd94d60 100644 --- a/public/images/text3.svg +++ b/frontend-electron/assets/images/text3.svg @@ -1,19 +1,19 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/public/images/text32.png b/frontend-electron/assets/images/text32.png similarity index 100% rename from public/images/text32.png rename to frontend-electron/assets/images/text32.png diff --git a/public/images/top24.png b/frontend-electron/assets/images/top24.png similarity index 100% rename from public/images/top24.png rename to frontend-electron/assets/images/top24.png diff --git a/public/images/underline24.png b/frontend-electron/assets/images/underline24.png similarity index 100% rename from public/images/underline24.png rename to frontend-electron/assets/images/underline24.png diff --git a/public/images/unlock20.png b/frontend-electron/assets/images/unlock20.png similarity index 100% rename from public/images/unlock20.png rename to frontend-electron/assets/images/unlock20.png diff --git a/data.json b/frontend-electron/data.json similarity index 100% rename from data.json rename to frontend-electron/data.json diff --git a/frontend-electron/js/common/asset-base.js b/frontend-electron/js/common/asset-base.js new file mode 100644 index 0000000..9dcc090 --- /dev/null +++ b/frontend-electron/js/common/asset-base.js @@ -0,0 +1,41 @@ +(function () { + 'use strict'; + + var g = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this); + if (g.soonAsset) return; + + function computeAssetBase() { + if (typeof window === 'undefined' || !window.location) { + return '../assets/images/'; + } + var p = window.location.pathname || '/'; + if (p.indexOf('/pages/') !== -1 || /\.html$/i.test(p)) { + return '../assets/images/'; + } + return 'assets/images/'; + } + + function computeJsBase() { + if (typeof window === 'undefined' || !window.location) { + return '../js/'; + } + var p = window.location.pathname || '/'; + if (p.indexOf('/pages/') !== -1 || /\.html$/i.test(p)) { + return '../js/'; + } + return 'js/'; + } + + g.SOON_ASSET_BASE = computeAssetBase(); + g.SOON_JS_BASE = computeJsBase(); + + g.soonAsset = function (name) { + var n = String(name || '').replace(/^\/+/, ''); + return g.SOON_ASSET_BASE + n; + }; + + g.soonJs = function (rel) { + var n = String(rel || '').replace(/^\/+/, ''); + return g.SOON_JS_BASE + n; + }; +})(); diff --git a/lib/common/fabric-ext.js b/frontend-electron/js/common/fabric-ext.js similarity index 97% rename from lib/common/fabric-ext.js rename to frontend-electron/js/common/fabric-ext.js index f7072ec..1a17cde 100644 --- a/lib/common/fabric-ext.js +++ b/frontend-electron/js/common/fabric-ext.js @@ -1,164 +1,164 @@ -// Fabric.js 扩展类 - -(function (fabric) { - fabric.CurvedText = fabric.util.createClass(fabric.Object, { - type: 'curved-text', - diameter: 0, - kerning: 0, - text: '', - flipped: false, - fill: '#000', - fontFamily: 'Times New Roman', - fontSize: 24, // in px - fontWeight: 'normal', - fontStyle: '', // "normal", "italic" or "oblique". - cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), - strokeStyle: null, - strokeWidth: 0, - initialize: function (text, options) { - options || (options = {}); - if (typeof text === 'object' && text) { - for (let key in text) { - this[key] = text[key] - } - } else { - this.text = text; - } - this.callSuper('initialize', options); - this.set('lockUniScaling', true); - var canvas = this.getCircularText(); - this.cropCanvas(canvas); - this.set('width', canvas.width); - this.set('height', canvas.height); - }, - _getFontDeclaration: function () { - return [ - (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), - (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), - this.fontSize + 'px', - (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) - ].join(' '); - }, - cropCanvas: function (canvas) { - var ctx = canvas.getContext('2d'), - w = canvas.width, - h = canvas.height, - pix = { x: [], y: [] }, n, - imageData = ctx.getImageData(0, 0, w, h), - fn = function (a, b) { return a - b }; - for (var y = 0; y < h; y++) { - for (var x = 0; x < w; x++) { - if (imageData.data[((y * w + x) * 4) + 3] > 0) { - pix.x.push(x); - pix.y.push(y); - } - } - } - // 如果没有找到任何非透明像素,直接返回,不进行裁剪 - if (pix.x.length === 0 || pix.y.length === 0) { - return; - } - pix.x.sort(fn); - pix.y.sort(fn); - n = pix.x.length - 1; - w = pix.x[n] - pix.x[0]; - h = pix.y[n] - pix.y[0]; - // 确保宽度和高度是有效的正数 - if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { - return; - } - var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); - canvas.width = w; - canvas.height = h; - ctx.putImageData(cut, 0, 0); - }, - getCircularText: function () { - var text = this.text, - diameter = this.diameter, - flipped = this.flipped, - kerning = this.kerning, - fill = this.fill, - inwardFacing = true, - startAngle = 0, - canvas = fabric.util.createCanvasElement(), - ctx = canvas.getContext('2d'), - cw, // character-width - x, // iterator - clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise - if (flipped) { - // startAngle = 180; - inwardFacing = false; - } - startAngle *= Math.PI / 180; // convert to radians - var d = document.createElement('div'); - d.style.fontFamily = this.fontFamily; - d.style.fontSize = this.fontSize + 'px'; - d.style.fontWeight = this.fontWeight; - d.style.fontStyle = this.fontStyle; - d.textContent = text; - document.body.appendChild(d); - var textHeight = d.offsetHeight; - document.body.removeChild(d); - canvas.width = canvas.height = diameter; - ctx.font = this._getFontDeclaration(); - if (inwardFacing) { text = text.split('').reverse().join('') }; - ctx.translate(diameter / 2, diameter / 2); // Move to center - startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward - ctx.textBaseline = 'middle'; // Ensure we draw in exact center - ctx.textAlign = 'center'; // Ensure we draw in exact center - for (x = 0; x < text.length; x++) { - cw = ctx.measureText(text[x]).width; - startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; - } - ctx.rotate(startAngle); - for (x = 0; x < text.length; x++) { - cw = ctx.measureText(text[x]).width; // half letter - ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); - if (this.strokeStyle && this.strokeWidth) { - ctx.strokeStyle = this.strokeStyle; - ctx.lineWidth = this.strokeWidth; - ctx.miterLimit = 2; - ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); - } - ctx.fillStyle = fill; - ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); - ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter - } - return canvas; - }, - _set: function (key, value) { - switch (key) { - case 'scaleX': - this.fontSize *= value; - this.diameter *= value; - this.width *= value; - this.scaleX = 1; - if (this.width < 1) { this.width = 1; } - break; - case 'scaleY': - this.height *= value; - this.scaleY = 1; - if (this.height < 1) { this.height = 1; } - break; - default: - this.callSuper('_set', key, value); - break; - } - }, - _render: function (ctx) { - var canvas = this.getCircularText(); - this.cropCanvas(canvas); - this.set('width', canvas.width); - this.set('height', canvas.height); - ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); - this.setCoords(); - }, - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); - } - }); - fabric.CurvedText.fromObject = function (object, callback, forceAsync) { - return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); - }; -})(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); - +// Fabric.js 扩展类 + +(function (fabric) { + fabric.CurvedText = fabric.util.createClass(fabric.Object, { + type: 'curved-text', + diameter: 0, + kerning: 0, + text: '', + flipped: false, + fill: '#000', + fontFamily: 'Times New Roman', + fontSize: 24, // in px + fontWeight: 'normal', + fontStyle: '', // "normal", "italic" or "oblique". + cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), + strokeStyle: null, + strokeWidth: 0, + initialize: function (text, options) { + options || (options = {}); + if (typeof text === 'object' && text) { + for (let key in text) { + this[key] = text[key] + } + } else { + this.text = text; + } + this.callSuper('initialize', options); + this.set('lockUniScaling', true); + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + }, + _getFontDeclaration: function () { + return [ + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + cropCanvas: function (canvas) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + pix = { x: [], y: [] }, n, + imageData = ctx.getImageData(0, 0, w, h), + fn = function (a, b) { return a - b }; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + if (imageData.data[((y * w + x) * 4) + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // 如果没有找到任何非透明像素,直接返回,不进行裁剪 + if (pix.x.length === 0 || pix.y.length === 0) { + return; + } + pix.x.sort(fn); + pix.y.sort(fn); + n = pix.x.length - 1; + w = pix.x[n] - pix.x[0]; + h = pix.y[n] - pix.y[0]; + // 确保宽度和高度是有效的正数 + if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { + return; + } + var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); + canvas.width = w; + canvas.height = h; + ctx.putImageData(cut, 0, 0); + }, + getCircularText: function () { + var text = this.text, + diameter = this.diameter, + flipped = this.flipped, + kerning = this.kerning, + fill = this.fill, + inwardFacing = true, + startAngle = 0, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'), + cw, // character-width + x, // iterator + clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise + if (flipped) { + // startAngle = 180; + inwardFacing = false; + } + startAngle *= Math.PI / 180; // convert to radians + var d = document.createElement('div'); + d.style.fontFamily = this.fontFamily; + d.style.fontSize = this.fontSize + 'px'; + d.style.fontWeight = this.fontWeight; + d.style.fontStyle = this.fontStyle; + d.textContent = text; + document.body.appendChild(d); + var textHeight = d.offsetHeight; + document.body.removeChild(d); + canvas.width = canvas.height = diameter; + ctx.font = this._getFontDeclaration(); + if (inwardFacing) { text = text.split('').reverse().join('') }; + ctx.translate(diameter / 2, diameter / 2); // Move to center + startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward + ctx.textBaseline = 'middle'; // Ensure we draw in exact center + ctx.textAlign = 'center'; // Ensure we draw in exact center + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; + startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; + } + ctx.rotate(startAngle); + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; // half letter + ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); + if (this.strokeStyle && this.strokeWidth) { + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.miterLimit = 2; + ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + } + ctx.fillStyle = fill; + ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter + } + return canvas; + }, + _set: function (key, value) { + switch (key) { + case 'scaleX': + this.fontSize *= value; + this.diameter *= value; + this.width *= value; + this.scaleX = 1; + if (this.width < 1) { this.width = 1; } + break; + case 'scaleY': + this.height *= value; + this.scaleY = 1; + if (this.height < 1) { this.height = 1; } + break; + default: + this.callSuper('_set', key, value); + break; + } + }, + _render: function (ctx) { + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); + this.setCoords(); + }, + toObject: function (propertiesToInclude) { + return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); + } + }); + fabric.CurvedText.fromObject = function (object, callback, forceAsync) { + return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); + }; +})(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); + diff --git a/lib/common/language.js b/frontend-electron/js/common/language.js similarity index 97% rename from lib/common/language.js rename to frontend-electron/js/common/language.js index d12a0b0..dc7bf1c 100644 --- a/lib/common/language.js +++ b/frontend-electron/js/common/language.js @@ -1,56 +1,56 @@ -// 多语言支持模块 - -/** - * 多语言切换 - */ -function langua_ge(lan = 'zh') { - $("[language='m']").each(function (i) { - $(this).html($(this).attr(lan)); - }); - - $("[language='t']").each(function (i) { - $(this).attr("title", $(this).attr(lan)); - }); -} - -/** - * 多语言字符串获取 - * @param {string} str - 字符串键 - * @param {string} s_lan - 当前语言 ('zh', 'ozh', 'en') - */ -function language_str(str, s_lan) { - let t = { - "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, - "save": { zh: "保存", ozh: "保存", en: "Save" }, - "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, - "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, - "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, - "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, - "display": { zh: "预览", ozh: "預覽", en: "Display" }, - "output": { zh: "导出", ozh: "導出", en: "Export" }, - "bg": { zh: "背景", ozh: "背景", en: "Background" }, - "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, - "img": { zh: "圖片", ozh: "圖片", en: "Image" }, - "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, - "text": { zh: "文本", ozh: "文本", en: "Text" }, - "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, - "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, - "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, - "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, - "line": { zh: "直線", ozh: "直線", en: "Line" }, - "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, - "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, - "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, - "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, - "vers": { zh: "版本", ozh: "版本", en: "Version" }, - "comf": { zh: "确认", ozh: "確認", en: "Select" }, - "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } - }; - return t[str] ? t[str][s_lan] : str; -} - -module.exports = { - langua_ge, - language_str -}; - +// 多语言支持模块 + +/** + * 多语言切换 + */ +function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)); + }); + + $("[language='t']").each(function (i) { + $(this).attr("title", $(this).attr(lan)); + }); +} + +/** + * 多语言字符串获取 + * @param {string} str - 字符串键 + * @param {string} s_lan - 当前语言 ('zh', 'ozh', 'en') + */ +function language_str(str, s_lan) { + let t = { + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, + "save": { zh: "保存", ozh: "保存", en: "Save" }, + "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, + "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, + "display": { zh: "预览", ozh: "預覽", en: "Display" }, + "output": { zh: "导出", ozh: "導出", en: "Export" }, + "bg": { zh: "背景", ozh: "背景", en: "Background" }, + "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, + "img": { zh: "圖片", ozh: "圖片", en: "Image" }, + "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, + "text": { zh: "文本", ozh: "文本", en: "Text" }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, + "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, + "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, + "line": { zh: "直線", ozh: "直線", en: "Line" }, + "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, + "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, + "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, + "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, + "vers": { zh: "版本", ozh: "版本", en: "Version" }, + "comf": { zh: "确认", ozh: "確認", en: "Select" }, + "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + }; + return t[str] ? t[str][s_lan] : str; +} + +module.exports = { + langua_ge, + language_str +}; + diff --git a/lib/common/utils.js b/frontend-electron/js/common/utils.js similarity index 95% rename from lib/common/utils.js rename to frontend-electron/js/common/utils.js index fba453e..c8f6d38 100644 --- a/lib/common/utils.js +++ b/frontend-electron/js/common/utils.js @@ -1,212 +1,212 @@ -// 公共工具函数模块 - -/** - * DES加密函数 - */ -function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { - var APIFMS; - try { - var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); - var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7 - }); - APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); - } catch (err) { - console.log('des 加密 -------------------------'); - console.log(err); - } - return APIFMS; -} - -/** - * 获取URL参数 - */ -function GetFile() { - const params = new URLSearchParams(window.location.search); - return params; -} - -/** - * 日期格式化 - */ -function getDate() { - let date = new Date(); - return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); -} - -/** - * 获取对象的绝对坐标 - */ -function getAbsoluteXY(_obj) { - let coord = _obj.get("lineCoords"); - return [_obj.get("left"), _obj.get("top")]; -} - -/** - * 计算两点之间的距离 - */ -function getDisdance(x1, y1, x2, y2) { - var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 - var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 - var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 - return distance; -} - -/** - * 计算角度(相对于画布中心) - */ -function getDeg(pointer, canvas) { - // 计算点击位置与画布中心的坐标差 - var centerX = canvas.width / 2; - var centerY = canvas.height / 2; - var mouseX = pointer.x; - var mouseY = pointer.y; - var dx = mouseX - centerX; - var dy = mouseY - centerY; - // 计算旋转角度(弧度) - var angleRad = Math.atan2(dy, dx); - // 将角度转换为度数 - var angleDeg = angleRad * (180 / Math.PI); - if (angleDeg < 0) { - angleDeg += 360; - } - angleDeg += 90; - return angleDeg; -} - -/** - * 检查字段名是否重复 - */ -function checkName(name, objs1, objs2) { - //检查字段名是否重复了 - for (let item of objs1) { - if (item.name == name) { - return false; - } - } - for (let item of objs2) { - if (item.name == name) { - return false; - } - } - return true; -} - -/** - * 返回不重复的字段名 - */ -function resName(pre_name, objs1, objs2) { - //返回的是不重复的字段名 - let num = (objs1.length + objs2.length) - 1; - do { - num++; - } - while (!checkName(pre_name + num, objs1, objs2));//重复了就再加1 - return pre_name + num; -} - -/** - * 获取对象在画布中的索引 - */ -function getIndex(target, canvas, _canvas = null) { - let temp_objs; - if (_canvas == null) { - temp_objs = canvas.getObjects(); - } else { - temp_objs = _canvas.getObjects(); - } - - let i = 0; - for (let res of temp_objs) { - if (target == res) { - return i; - } - i++; - } - return 0; -} - -/** - * 获取对象类型 - */ -function getType(target, canvas, objs) { - let temp_objs = canvas.getObjects(); - let i = 0; - for (let res of temp_objs) { - if (target == res) { - return objs[i].type; - } - i++; - } -} - -/** - * 获取坐标的最小X值 - */ -function getCoordsMinX(acoords) { - let x = acoords[0].x; - for (let item of acoords) { - if (item.x < x) { - x = item.x; - } - } - return x; -} - -/** - * 获取坐标的最大X值 - */ -function getCoordsMaxX(acoords) { - let x = acoords[0].x; - for (let item of acoords) { - if (item.x > x) { - x = item.x; - } - } - return x; -} - -/** - * 获取坐标的最小Y值 - */ -function getCoordsMinY(acoords) { - let y = acoords[0].y; - for (let item of acoords) { - if (item.y < y) { - y = item.y; - } - } - return y; -} - -/** - * 获取坐标的最大Y值 - */ -function getCoordsMaxY(acoords) { - let y = acoords[0].y; - for (let item of acoords) { - if (item.y > y) { - y = item.y; - } - } - return y; -} - -module.exports = { - desEncrypt, - GetFile, - getDate, - getAbsoluteXY, - getDisdance, - getDeg, - checkName, - resName, - getIndex, - getType, - getCoordsMinX, - getCoordsMaxX, - getCoordsMinY, - getCoordsMaxY -}; - +// 公共工具函数模块 + +/** + * DES加密函数 + */ +function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + var APIFMS; + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + } catch (err) { + console.log('des 加密 -------------------------'); + console.log(err); + } + return APIFMS; +} + +/** + * 获取URL参数 + */ +function GetFile() { + const params = new URLSearchParams(window.location.search); + return params; +} + +/** + * 日期格式化 + */ +function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); +} + +/** + * 获取对象的绝对坐标 + */ +function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")]; +} + +/** + * 计算两点之间的距离 + */ +function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; +} + +/** + * 计算角度(相对于画布中心) + */ +function getDeg(pointer, canvas) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90; + return angleDeg; +} + +/** + * 检查字段名是否重复 + */ +function checkName(name, objs1, objs2) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; +} + +/** + * 返回不重复的字段名 + */ +function resName(pre_name, objs1, objs2) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num, objs1, objs2));//重复了就再加1 + return pre_name + num; +} + +/** + * 获取对象在画布中的索引 + */ +function getIndex(target, canvas, _canvas = null) { + let temp_objs; + if (_canvas == null) { + temp_objs = canvas.getObjects(); + } else { + temp_objs = _canvas.getObjects(); + } + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; +} + +/** + * 获取对象类型 + */ +function getType(target, canvas, objs) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } +} + +/** + * 获取坐标的最小X值 + */ +function getCoordsMinX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x; + } + } + return x; +} + +/** + * 获取坐标的最大X值 + */ +function getCoordsMaxX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x; + } + } + return x; +} + +/** + * 获取坐标的最小Y值 + */ +function getCoordsMinY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y; + } + } + return y; +} + +/** + * 获取坐标的最大Y值 + */ +function getCoordsMaxY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y; + } + } + return y; +} + +module.exports = { + desEncrypt, + GetFile, + getDate, + getAbsoluteXY, + getDisdance, + getDeg, + checkName, + resName, + getIndex, + getType, + getCoordsMinX, + getCoordsMaxX, + getCoordsMinY, + getCoordsMaxY +}; + diff --git a/lib/design1-back.js b/frontend-electron/js/design1-back.js similarity index 98% rename from lib/design1-back.js rename to frontend-electron/js/design1-back.js index 0a91287..c89a050 100644 --- a/lib/design1-back.js +++ b/frontend-electron/js/design1-back.js @@ -1,5781 +1,5781 @@ - - function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { - var APIFMS; - try { - var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); - var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7 - }); - APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); - } catch (err) { - console.log('des 加密 -------------------------'); - console.log(err); - } - return APIFMS; - } - (function (fabric) { - fabric.CurvedText = fabric.util.createClass(fabric.Object, { - type: 'curved-text', - diameter: 0, - kerning: 0, - text: '', - flipped: false, - fill: '#000', - fontFamily: 'Times New Roman', - fontSize: 24, // in px - fontWeight: 'normal', - fontStyle: '', // "normal", "italic" or "oblique". - cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), - strokeStyle: null, - strokeWidth: 0, - initialize: function (text, options) { - options || (options = {}); - if (typeof text === 'object' && text) { - for (let key in text) { - this[key] = text[key] - } - } else { - this.text = text; - } - this.callSuper('initialize', options); - this.set('lockUniScaling', true); - var canvas = this.getCircularText(); - this.cropCanvas(canvas); - this.set('width', canvas.width); - this.set('height', canvas.height); - }, - _getFontDeclaration: function () { - return [ - (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), - (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), - this.fontSize + 'px', - (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) - ].join(' '); - }, - cropCanvas: function (canvas) { - var ctx = canvas.getContext('2d'), - w = canvas.width, - h = canvas.height, - pix = { x: [], y: [] }, n, - imageData = ctx.getImageData(0, 0, w, h), - fn = function (a, b) { return a - b }; - for (var y = 0; y < h; y++) { - for (var x = 0; x < w; x++) { - if (imageData.data[((y * w + x) * 4) + 3] > 0) { - pix.x.push(x); - pix.y.push(y); - } - } - } - // 如果没有找到任何非透明像素,直接返回,不进行裁剪 - if (pix.x.length === 0 || pix.y.length === 0) { - return; - } - pix.x.sort(fn); - pix.y.sort(fn); - n = pix.x.length - 1; - w = pix.x[n] - pix.x[0]; - h = pix.y[n] - pix.y[0]; - // 确保宽度和高度是有效的正数 - if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { - return; - } - var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); - canvas.width = w; - canvas.height = h; - ctx.putImageData(cut, 0, 0); - }, - getCircularText: function () { - var text = this.text, - diameter = this.diameter, - flipped = this.flipped, - kerning = this.kerning, - fill = this.fill, - inwardFacing = true, - startAngle = 0, - canvas = fabric.util.createCanvasElement(), - ctx = canvas.getContext('2d'), - cw, // character-width - x, // iterator - clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise - if (flipped) { - // startAngle = 180; - inwardFacing = false; - } - startAngle *= Math.PI / 180; // convert to radians - var d = document.createElement('div'); - d.style.fontFamily = this.fontFamily; - d.style.fontSize = this.fontSize + 'px'; - d.style.fontWeight = this.fontWeight; - d.style.fontStyle = this.fontStyle; - d.textContent = text; - document.body.appendChild(d); - var textHeight = d.offsetHeight; - document.body.removeChild(d); - canvas.width = canvas.height = diameter; - ctx.font = this._getFontDeclaration(); - if (inwardFacing) { text = text.split('').reverse().join('') }; - ctx.translate(diameter / 2, diameter / 2); // Move to center - startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward - ctx.textBaseline = 'middle'; // Ensure we draw in exact center - ctx.textAlign = 'center'; // Ensure we draw in exact center - for (x = 0; x < text.length; x++) { - cw = ctx.measureText(text[x]).width; - startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; - } - ctx.rotate(startAngle); - for (x = 0; x < text.length; x++) { - cw = ctx.measureText(text[x]).width; // half letter - ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); - if (this.strokeStyle && this.strokeWidth) { - ctx.strokeStyle = this.strokeStyle; - ctx.lineWidth = this.strokeWidth; - ctx.miterLimit = 2; - ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); - } - ctx.fillStyle = fill; - ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); - ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter - } - return canvas; - }, - _set: function (key, value) { - switch (key) { - case 'scaleX': - this.fontSize *= value; - this.diameter *= value; - this.width *= value; - this.scaleX = 1; - if (this.width < 1) { this.width = 1; } - break; - case 'scaleY': - this.height *= value; - this.scaleY = 1; - if (this.height < 1) { this.height = 1; } - break; - default: - this.callSuper('_set', key, value); - break; - } - }, - _render: function (ctx) { - var canvas = this.getCircularText(); - this.cropCanvas(canvas); - this.set('width', canvas.width); - this.set('height', canvas.height); - ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); - this.setCoords(); - }, - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); - } - }); - fabric.CurvedText.fromObject = function (object, callback, forceAsync) { - return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); - }; - })(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); - - const remote = require('@electron/remote') - var path = require('path'); - // 使用用户数据目录,避免写入需要管理员权限 - const exePath = remote.app.getPath('userData') - const { ipcRenderer } = require('electron') - const { dialog } = require('@electron/remote') - var jrQrcode = require('jr-qrcode'); - var JsBarcode = require('jsbarcode'); - ipcRenderer.send('get-sys-fonts'); - ipcRenderer.send('get-scale-rate'); - const { clipboard } = require('electron') - const dpi = 600 - // // 内部剪贴板 - // let clipboardData = { - // objects: null, - // offset: 10 // 粘贴位置偏移量 - // }; - - layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { - let myDate = new Date(); - let s_lan = ""; - ipcRenderer.send('get-sys-language'); - /* if(myDate.getFullYear()>2021 && myDate.getMonth() >= 1 && myDate.getDate() > 15){ - - }*/ - function GetFile() { - const params = new URLSearchParams(window.location.search); - return params - } - function langua_ge(lan = 'zh') { - $("[language='m']").each(function (i) { - $(this).html($(this).attr(lan)); - }) - - $("[language='t']").each(function (i) { - $(this).attr("title", $(this).attr(lan)); - }) - } - function language_str(str) { - let t = { - "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, - "save": { zh: "保存", ozh: "保存", en: "Save" }, - "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, - "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, - "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, - "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, - "display": { zh: "预览", ozh: "預覽", en: "Display" }, - "output": { zh: "导出", ozh: "導出", en: "Export" }, - "bg": { zh: "背景", ozh: "背景", en: "Background" }, - "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, - "selectPic": { zh: "请选择文件", ozh: "請選擇文件", en: "Please select a file" }, - "img": { zh: "圖片", ozh: "圖片", en: "Image" }, - "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, - "text": { zh: "文本", ozh: "文本", en: "Text" }, - "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, - "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, - "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, - "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, - "line": { zh: "直線", ozh: "直線", en: "Line" }, - "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, - "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, - "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, - "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, - "vers": { zh: "版本", ozh: "版本", en: "Version" }, - "comf": { zh: "确认", ozh: "確認", en: "Select" }, - "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } - } - return t[str][s_lan]; - } - - var $ = layui.$; - var layer = layui.layer - , slider = layui.slider, colorpicker = layui.colorpicker; - let addState = 0;//0是不添加 - let background_image, background_image1, background_image2; - let bg_version = 1;//front_bg1.png front_bg2.png - var zoom = 1; - $(function () { - // 定义CurvedText类 - - - $("#object_attribute").css("height", "470px"); - $("#obj_list").css("height", document.body.clientHeight - 520); - $(".obj_list").css("height", $("#obj_list").height() - 32); - - $(".main").show(); - $(".canvas_box").css("height", document.body.clientHeight - 100); - zoom = $(".canvas_bg_img").width() / 2787 - $("#canvas1").attr("width", $(".canvas_box").width() / zoom); - $("#canvas1").attr("height", $(".canvas_box").height() / zoom); - $("#canvas2").attr("width", $(".canvas_box").width() / zoom); - $("#canvas2").attr("height", $(".canvas_box").height() / zoom); - var openAs = { - _name: "", - set name(val) { - if (val == "") { - $("title").html('Soon Design'); - } else { - $("title").html('Soon Design - ' + val); - } - this._name = val; - }, - get name() { - return this._name - }, - };//被打开的文件名 - let objs1 = []; - let objs2 = []; - let step1 = { val: 0 }, step2 = { val: 0 }; - let step = step1; - let objs = objs1; - let recordJson1 = [], recordJson2 = [], recordJson = recordJson1, recordObjs1 = [], recordObjs2 = [], recordObjs = recordObjs1; - let pre_objs1 = [], pre_objs2 = [], next_objs1 = [], next_objs2 = [], pre_objs = pre_objs1, next_objs = next_objs1; - let pre_json1 = [], pre_json2 = [], next_json1 = [], next_json2 = [], pre_json = pre_json1, next_json = next_json1; - var canvas1 = new fabric.Canvas('canvas1'); - var canvas2 = new fabric.Canvas('canvas2'); - fabric.Object.prototype.objectCaching = false; - var ctx1 = canvas1.getSelectionContext(); - var ctx2 = canvas2.getSelectionContext(); - let is_bgi_add = false; - canvas1.zoomToPoint(new fabric.Point(0, 0), 1); - canvas2.zoomToPoint(new fabric.Point(0, 0), 1); - - // 初始化标尺 - initRulers(); - - $("#source_front").width(zoom * $("#source_front").width()); - $("#source_back").width(zoom * $("#source_back").width()); - $(".canvas-container:eq(0)").css('transform', 'scale(' + zoom + ')') - $(".canvas-container:eq(1)").css('transform', 'scale(' + zoom + ')') - $(".canvas-container:eq(0)").css('width', 0) - - $(".canvas-container:eq(1)").hide(); - var canvas = canvas1; - fabric.Object.prototype.set({ - borderColor: "#9013FE", - cornerColor: "#9013FE", //激活状态角落图标的填充颜色 - cornerStrokeColor: "#9013FE", //激活状态角落图标的边框颜色 - borderOpacityWhenMoving: 1, - borderScaleFactor: 6, - cornerSize: 24, - cornerStyle: "circle", //rect,circle - centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 - centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 - transparentCorners: false, //激活状态角落的图标是否透明 - rotatingPointOffset: 20, //旋转距旋转体的距离 - padding: 0, - }); - canvas1.preserveObjectStacking = true; - canvas2.preserveObjectStacking = true; - - - var front_list = []; - ipcRenderer.on('font-list', (event, data) => { - const newFontList = []; - data.map(item => { - if (item.indexOf('"') === 0) { - newFontList.push(item.replace(/^"|"$/g, '')); - } else { - newFontList.push(item); - } - }) - front_list = newFontList; - for (let item of front_list) { - $("#text_font_family").append(""); - $("#circle_text_font_family").append(""); - $("#out_text_font_family").append(""); - $("#barcode_font_family").append(""); - $("#count_font_family").append(""); - } - }); - ipcRenderer.on('sys-lan', (event, data) => { - let lang = localStorage.getItem("lang"); - if (lang) { - s_lan = lang; - langua_ge(lang); - $("#language_select").val(lang); - return; - } - switch (data) { - case 'zh-CN': - s_lan = "zh"; - langua_ge("zh"); - $("#language_select").val("zh"); - localStorage.setItem("lang", "zh"); - break; - case 'zh': - s_lan = "zh"; - langua_ge("zh"); - $("#language_select").val("zh"); - localStorage.setItem("lang", "zh"); - break; - case 'zh-TW': - s_lan = "ozh"; - langua_ge("ozh"); - $("#language_select").val("ozh"); - localStorage.setItem("lang", "ozh"); - break; - case 'en': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - case 'en-AU': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - case 'en-CA': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - case 'en-GB': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - case 'en-NZ': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - case 'en-US': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - case 'en-ZA': - s_lan = "en"; - langua_ge("en"); - $("#language_select").val("en"); - localStorage.setItem("lang", "en"); - break; - } - }); - ipcRenderer.on('close', (event, message) => { - layer.confirm('' + language_str("whetherSave") + '', {//是否保存当前文件? - btn: [language_str("save"), language_str("noSave"), language_str("cancel")] //['保存', '不保存', '取消'] - , btn3: function (index, layero) { - - } - }, function (index, layero) { - - output(() => ipcRenderer.send('run-close')); - return; - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let _g1 = [], _g2 = []; - let left = 0, top = 0; - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g1.push(fabric.util.object.clone(_o)); - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - left = 0, top = 0; - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g2.push(fabric.util.object.clone(_o)); - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; - if (openAs.name != "") { - //打开的文件 - let fs = require('fs') - o.soonType = 1 - fs.writeFileSync(openAs.name, JSON.stringify(o)) - layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' - saveHistory(); - - ipcRenderer.send('run-close'); - return; - } - dialog.showSaveDialog({ - title: language_str("saveFile"),//'保存文件' - filters: [ - { name: 'Soon File Type', extensions: ['soon'] }, - ] - }).then(result => { - if (result.filePath == "") { ipcRenderer.send('run-close'); return; } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon'; - } - let fs = require('fs') - o.soonType = 1 - fs.writeFileSync(result.filePath, JSON.stringify(o)) - openAs.name = result.filePath; - layer.msg(language_str("saveSucc") + openAs.name);//保存成功至 - saveHistory(); - ipcRenderer.send('run-close'); - }).catch(err => { - console.log() - }) - layer.close(index); - }, function (index) { - ipcRenderer.send('run-close'); - }); - - }) - addBackground();//添加背景 白色卡片 - - initAligningGuidelines(canvas1); - initAligningGuidelines(canvas2); - //canvas.setZoom(0.5) - //recordObjs1.push(JSON.stringify(objs1)); - //recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - //recordObjs2.push(JSON.stringify(objs2)); - //recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - canvas.renderAll();//重新渲染画布 - var zoomSlider = slider.render({ - elem: '#zoomSlide', //绑定元素 - min: 25, - max: 500, - value: 100, - change(val) { - console.log(val, zoomSlider) - canvas.zoomToPoint( - new fabric.Point(0, 0), - val / 100 - ); - // zoom = val / 100; - //$(".canvas-container").css("transform","scale(" + val / 100 + ")") - } - - }); - window.rotate = function (id) { - let angle = $(`#${id}`).attr("angle") - 0; - angle += 90; - $(`#${id}`).css("transform", `rotate(${angle}deg)`); - $(`#${id}`).attr("angle", angle); - } - $('#display').on('click', function () { - // 类型改变 - fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_1.png', function (i1) { - i1.left = background_image.left; - i1.top = background_image.top; - let img1 = i1; - - fabric.Image.fromURL('./public/images/back_bg_1.png', function (i2) { - i2.left = background_image.left; - i2.top = background_image.top; - let img2 = i2; - fabric.Image.fromURL('./public/images/op_1.png', function (i3) { - i2.left = background_image.left; - i2.top = background_image.top; - let img3 = i3; - display_func(img1, img2, img3); - }) - }) - }) - }); - // 导出文件 - async function saveImageAsPNG(buffer) { - let fs = require('fs'); - // 弹出保存对话框 - const { canceled, filePath } = await dialog.showSaveDialog({ - title: '保存图片', - defaultPath: path.join(exePath, 'output.png'), // 默认路径 - filters: [{ name: 'PNG Images', extensions: ['png'] }] // 文件过滤器 - }); - - if (canceled) { - console.log('用户取消了保存操作'); - return; - } - - // 将Buffer写入PNG文件到用户选择的路径 - fs.writeFile(filePath, buffer, (err) => { - if (err) { - console.error('写入文件失败:', err); - } else { - layer.msg(language_str("saveSucc") + filePath);//'保存成功至' - console.log('PNG文件已保存至:', filePath); - } - }); - } - function display_func(img1, img2, img3) { - $("#base_control").hide(); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//"背景" - canvas1.discardActiveObject().renderAll(); - canvas2.discardActiveObject().renderAll(); - let _objs1 = canvas1.getObjects(); - let g1 = []; - let g3 = []; - let g5 = []; - let left = 0;//左裁剪 - let top = 0;//顶裁剪 - let black_left = 0;//黑色左裁剪 - let black_top = 0;//黑色顶裁剪 - let all_left = 0;//预览图左裁剪 - let all_top = 0;//预览图顶裁剪 - let list1 = [..._objs1] - for (let item of list1) { - let idx = getIndex(item, canvas1); - //console.log("------"); - //console.log(item.getCoords()); - //console.log(getCoordsMinX(background_image.getCoords())); - //console.log(getCoordsMinX(item.getCoords())); - //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); - //console.log("------"); - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g3.push(fabric.util.object.clone(item)); - if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { - continue;//计数器 或 合成文字 - } - if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { - continue;//二维码 条形码 - } - if (idx != 0 && (objs1[idx - 1].type == 2)) { - continue;//合成图片 - } - if (idx != 0 && - ( - ( - objs1[idx - 1].type == 3 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //文字 && 黑色 - ) - || - ( - objs1[idx - 1].type == 11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) - || - ( - objs1[idx - 1].type == 5//矩形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs1[idx - 1].type == 6//圆形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs1[idx - 1].type == 7//直线 - && - (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") - ) - || - objs1[idx - 1].black == true //勾选并入黑色图层 - // ( - // objs1[idx-1].type == 1//固定图片 - // && - // objs1[idx-1].black == true //勾选并入黑色图层 - // ) - ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g1.push(fabric.util.object.clone(item)); - continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g1.push(fabric.util.object.clone(item)); - } - - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g1.push(fabric.util.object.clone(item)); - } - let url1 = new fabric.Group(g1).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - - g3.push(img1); - let url3 = new fabric.Group(g3).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - let url5 = new fabric.Group(g5).toDataURL({ - height: 2787, - width: 2787, - top: black_top, - left: black_left - }); - let _objs2 = canvas2.getObjects(); - let g2 = []; - let g4 = []; - let g6 = []; - left = 0; - top = 0; - black_top = 0; - black_left = 0; - all_top = 0; - all_left = 0; - for (let item of _objs2) { - let idx = getIndex(item, canvas2); - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g4.push(fabric.util.object.clone(item)); - if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { - continue;//黑色字或计数器 或 合成文字 - } - if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { - continue;//二维码 条形码 - } - if (idx != 0 && (objs2[idx - 1].type == 2)) { - continue;//合成图片 - } - if (idx != 0 && (objs2[idx - 1].type == 10)) { - continue;//合成图片 - } - if (idx != 0 && - ( - ( - objs2[idx - 1].type == 3 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //文字 && 黑色 - ) - || - ( - objs2[idx - 1].type == 11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) - || - ( - objs2[idx - 1].type == 5//矩形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs2[idx - 1].type == 6//圆形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs2[idx - 1].type == 7//直线 - && - (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") - ) - || - objs2[idx - 1].black == true //勾选并入黑色图层 - // ( - // objs2[idx-1].type == 1//固定图片 - // && - // objs2[idx-1].black == true //勾选并入黑色图层 - // ) - ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g2.push(fabric.util.object.clone(item)); - continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g2.push(fabric.util.object.clone(item)); - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g2.push(fabric.util.object.clone(item)); - - } - let url2 = new fabric.Group(g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let white_url1 = canvas1.getObjects()[0].toDataURL({ - height: 2787, - width: 2787, - }); - g4.push(img2); - let url4 = new fabric.Group(g4).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - let url6 = new fabric.Group(g6).toDataURL({ - height: 2787, - width: 2787, - top: black_top, - left: black_left - }); - - let w = document.body.clientWidth * 0.7 + "px"; - let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; - let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; - - if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { - //高度超出,则按高度计算 - h = document.body.clientHeight * 0.8 + "px"; - h1 = document.body.clientHeight * 0.8 - 150 + "px"; - w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 - } - - let fs = require('fs'); - const Buffer = require('buffer').Buffer; - const buffer = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); - const printPath = path.join(exePath, 'print.png'); - fs.writeFileSync(printPath, buffer); - - layer.open({ - type: 1, - area: [w, h], - title: language_str("display"), //不显示标题栏"预览" - shadeClose: true, //点击遮罩关闭 - content: ` -
-
-
- -
- -
-
-
-
`, - btn: [language_str("output"), "打印"],//'导出' - btn1: function (index, layero) { - saveImageAsPNG(buffer) - - printJS({ printable: printPath, type: 'image', style: 'img { width: 100%; height: auto; }' }) - } - }); - } - function output(callback = null, _save = save) { - // 类型改变 - fabric.Image.fromURL('./public/images/op_1.png', function (i1) { - i1.left = background_image.left; - i1.top = background_image.top; - let img1 = i1; - fabric.Image.fromURL('./public/images/op_1.png', function (i2) { - i2.left = background_image.left; - i2.top = background_image.top; - let img2 = i2; - - $("#base_control").hide(); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//"背景" - canvas1.discardActiveObject().renderAll(); - canvas2.discardActiveObject().renderAll(); - let _objs1 = canvas1.getObjects(); - let g1 = []; - let g3 = []; - let g5 = [];//黑色图层 - let left = 0;//左裁剪 - let top = 0;//顶裁剪 - let black_left = 0;//黑色左裁剪 - let black_top = 0;//黑色顶裁剪 - let all_left = 0;//预览图左裁剪 - let all_top = 0;//预览图顶裁剪 - for (let item of _objs1) { - let idx = getIndex(item, canvas1); - - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g3.push(fabric.util.object.clone(item)); - if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { - continue;//计数器 或 合成文字 - } - if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { - continue;//二维码 条形码 - } - if (idx != 0 && (objs1[idx - 1].type == 2)) { - continue;//合成图片 - } - if (idx != 0 && - ( - ( - objs1[idx - 1].type == 3 - && - item.get("fill") == "#000000" - //文字 && 黑色 - ) - || - ( - objs1[idx - 1].type == 11 - && - item.get("fill") == "#000000" - //圆形文字 && 黑色 - ) - || - ( - objs1[idx - 1].type == 5//矩形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs1[idx - 1].type == 6//圆形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs1[idx - 1].type == 7//直线 - && - (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") - ) - || - objs1[idx - 1].black == true //勾选并入黑色图层 - - ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g1.push(fabric.util.object.clone(item)); - continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g1.push(fabric.util.object.clone(item)); - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g1.push(fabric.util.object.clone(item)); - } - let url1 = new fabric.Group(g1).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - g3.push(img1); - let url3 = new fabric.Group(g3).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - let url5 = new fabric.Group(g5).toDataURL({ - height: 2787, - width: 2787, - top: black_top, - left: black_left - }); - let _objs2 = canvas2.getObjects(); - let g2 = []; - let g4 = []; - let g6 = [];//黑色图层 - left = 0; - top = 0; - black_left = 0;//黑色左裁剪 - black_top = 0;//黑色顶裁剪 - all_left = 0;//黑色左裁剪 - all_top = 0;//黑色顶裁剪 - for (let item of _objs2) { - let idx = getIndex(item, canvas2); - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g4.push(fabric.util.object.clone(item)); - if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { - continue;//计数器 或 合成文字 - } - if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { - continue;//二维码 条形码 - } - if (idx != 0 && (objs2[idx - 1].type == 2)) { - continue;//合成图片 - } - if (idx != 0 && - ( - ( - objs2[idx - 1].type == 3 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //文字 && 黑色 - ) - || - ( - objs2[idx - 1].type ==11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) - || - ( - objs2[idx - 1].type == 5//矩形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs2[idx - 1].type == 6//圆形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs2[idx - 1].type == 7//直线 - && - (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") - ) - || - objs2[idx - 1].black == true //勾选并入黑色图层 - // ( - // objs2[idx-1].type == 1//固定图片 - // && - // objs2[idx-1].black == true //勾选并入黑色图层 - // ) - ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g2.push(fabric.util.object.clone(item)); - continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g2.push(fabric.util.object.clone(item)); - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g2.push(fabric.util.object.clone(item)); - - } - let url2 = new fabric.Group(g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let white_url1 = canvas1.getObjects()[0].toDataURL({ - height: 2787, - width: 2787, - }); - g4.push(img2); - let url4 = new fabric.Group(g4).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - let url6 = new fabric.Group(g6).toDataURL({ - height: 2787, - width: 2787, - top: black_top, - left: black_left - }); - // let white_url2 = canvas2.getObjects()[0].toDataURL({ - // height: 2787, - // width: 2787, - // }); - //console.log(canvas.getObjects()) - let w = document.body.clientWidth * 0.7 + "px"; - let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; - let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; - - if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { - //高度超出,则按高度计算 - h = document.body.clientHeight * 0.8 + "px"; - h1 = document.body.clientHeight * 0.8 - 150 + "px"; - w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 - } - - let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; - if (objs1.length != 0 & objs2.length == 0) { - op.flag = 2; - } else if (objs1.length == 0 & objs2.length != 0) { - op.flag = 3; - } - op.frontColorPic = desEncrypt(url1); - op.backColorPic = ''; - //op.frontBlPic = url5; - //op.backBlPic = url6; - op.frontBlackPic = ''; - op.backBlackPic = ''; - for (let item of _objs1) { - let idx = getIndex(item, canvas1); - if (idx == 0) continue;//background-image - let type = objs1[idx - 1].type; - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 1, - "name": objs1[idx - 1].name, - "FontColor": objs1[idx - 1].black ? "#000000" : "@", - "transparency": item.get("opacity"), - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 3) { - - } else if (type == 11) { - - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs1[idx - 1].circleCenter - }); - } else if (type == 4) { - //out_text - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 3, - "name": objs1[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "DefaultText": item.get("text"), - "FontBackgroudColor": item.get("textBackgroundColor"), - "FontSize": parseInt(item.get("fontSize")), - "FontColor": item.get("fill"), - "Exceed": objs1[idx - 1].exceed, - "AutoWrap": objs1[idx - 1].autoWrap, - "FixSize": objs1[idx - 1].fixSize, - "Bold": item.get("fontWeight") == "bold", - "Underline": item.get("underline"), - "Italic": item.get("fontStyle") == "italic", - "Strikeout": item.get("linethrough"), - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)), - align: objs1[idx - 1].align - }); - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 4, - "name": objs1[idx - 1].name, - "FontColor": objs1[idx - 1].color, - "DefaultText": objs1[idx - 1].val, - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 9) { - //条形码 - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 5, - "name": objs1[idx - 1].name, - "FontSize": parseInt(objs1[idx - 1].fontSize), - "DefaultText": objs1[idx - 1].val, - "HideText": objs1[idx - 1].show, - "FontFamily": objs1[idx - 1].font, - "FontColor": objs1[idx - 1].color, - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 10) { - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 6, - "name": objs1[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "FontSize": parseInt(item.get("fontSize")), - "FontColor": item.get("fill"), - "start": objs1[idx - 1].start, - "add": parseInt(objs1[idx - 1].add), - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } - } - //obj2 - for (let item of _objs2) { - let idx = getIndex(item, canvas2); - if (idx == 0) continue;//background-image - let type = objs2[idx - 1].type; - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 1, - "name": objs2[idx - 1].name, - "FontColor": objs2[idx - 1].black ? "#000000" : "@", - "transparency": item.get("opacity"), - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 3) { - - } else if (type == 11) { - - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs2[idx - 1].circleCenter - }); - } else if (type == 4) { - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 3, - "name": objs2[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "DefaultText": item.get("text"), - "FontSize": parseInt(item.get("fontSize")), - "FontBackgroudColor": item.get("textBackgroundColor"), - "FontColor": item.get("fill"), - "Exceed": objs2[idx - 1].exceed, - "AutoWrap": objs2[idx - 1].autoWrap, - "FixSize": objs2[idx - 1].fixSize, - "Bold": item.get("fontWeight") == "bold", - "Underline": item.get("underline"), - "Italic": item.get("fontStyle") == "italic", - "Strikeout": item.get("linethrough"), - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)), - align: objs2[idx - 1].align - }); - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 4, - "name": objs2[idx - 1].name, - "FontColor": objs2[idx - 1].color, - "DefaultText": objs2[idx - 1].val, - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 9) { - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 5, - "name": objs2[idx - 1].name, - "DefaultText": objs2[idx - 1].val, - "HideText": objs1[idx - 1].show, - "FontSize": parseInt(objs2[idx - 1].fontSize), - "FontFamily": objs2[idx - 1].font, - "FontColor": objs2[idx - 1].color, - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 10) { - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 6, - "name": objs2[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "FontSize": parseInt(item.get("fontSize")), - "FontColor": item.get("fill"), - "start": parseInt(objs2[idx - 1].start), - "add": parseInt(objs2[idx - 1].add), - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } - } - _save(op, callback); - //return false 开启该代码可禁止点击该按钮关闭 - }) - }) - } - $('#output').on('click', function () { - output(null, saveAs); - return; - // 类型改变 - fabric.Image.fromURL('./public/images/op_1.png', function (i1) { - i1.left = background_image.left; - i1.top = background_image.top; - let img1 = i1; - fabric.Image.fromURL('./public/images/op_1.png', function (i2) { - i2.left = background_image.left; - i2.top = background_image.top; - let img2 = i2; - - $("#base_control").hide(); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//"背景" - canvas1.discardActiveObject().renderAll(); - canvas2.discardActiveObject().renderAll(); - let _objs1 = canvas1.getObjects(); - let g1 = []; - let g3 = []; - let g5 = [];//黑色图层 - let left = 0;//左裁剪 - let top = 0;//顶裁剪 - let black_left = 0;//黑色左裁剪 - let black_top = 0;//黑色顶裁剪 - let all_left = 0;//预览图左裁剪 - let all_top = 0;//预览图顶裁剪 - for (let item of _objs1) { - let idx = getIndex(item, canvas1); - - - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g3.push(fabric.util.object.clone(item)); - if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { - continue;//计数器 或 合成文字 - } - if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { - continue;//二维码 条形码 - } - if (idx != 0 && (objs1[idx - 1].type == 2)) { - continue;//合成图片 - } - if (idx != 0 && - ( - ( - objs1[idx - 1].type == 3 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //文字 && 黑色 - ) - || - ( - objs1[idx - 1].type == 11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) - || - ( - objs1[idx - 1].type == 5//矩形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs1[idx - 1].type == 6//圆形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs1[idx - 1].type == 7//直线 - && - (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") - ) - || - objs1[idx - 1].black == true //勾选并入黑色图层 - // ( - // objs1[idx-1].type == 1//固定图片 - // && - // objs1[idx-1].black == true //勾选并入黑色图层 - // ) - ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g1.push(fabric.util.object.clone(item)); - continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g1.push(fabric.util.object.clone(item)); - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g1.push(fabric.util.object.clone(item)); - } - let url1 = new fabric.Group(g1).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - g3.push(img1); - let url3 = new fabric.Group(g3).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - let url5 = new fabric.Group(g5).toDataURL({ - height: 2787, - width: 2787, - top: black_top, - left: black_left - }); - let _objs2 = canvas2.getObjects(); - let g2 = []; - let g4 = []; - let g6 = [];//黑色图层 - left = 0; - top = 0; - black_left = 0;//黑色左裁剪 - black_top = 0;//黑色顶裁剪 - all_left = 0;//黑色左裁剪 - all_top = 0;//黑色顶裁剪 - for (let item of _objs2) { - let idx = getIndex(item, canvas2); - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g4.push(fabric.util.object.clone(item)); - if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { - continue;//计数器 或 合成文字 - } - if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { - continue;//二维码 条形码 - } - if (idx != 0 && (objs2[idx - 1].type == 2)) { - continue;//合成图片 - } - if (idx != 0 && - ( - ( - objs2[idx - 1].type == 3 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //文字 && 黑色 - ) - || - ( - objs2[idx - 1].type == 11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) - || - ( - objs2[idx - 1].type == 5//矩形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs2[idx - 1].type == 6//圆形 - && - ( - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 - || - (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 - || - ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 - ) - ) - || - ( - objs2[idx - 1].type == 7//直线 - && - (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") - ) - || - objs2[idx - 1].black == true //勾选并入黑色图层 - // ( - // objs2[idx-1].type == 1//固定图片 - // && - // objs2[idx-1].black == true //勾选并入黑色图层 - // ) - ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g2.push(fabric.util.object.clone(item)); - continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g2.push(fabric.util.object.clone(item)); - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - g2.push(fabric.util.object.clone(item)); - - } - let url2 = new fabric.Group(g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let white_url1 = canvas1.getObjects()[0].toDataURL({ - height: 2787, - width: 2787, - }); - g4.push(img2); - let url4 = new fabric.Group(g4).toDataURL({ - height: 2787, - width: 2787, - top: all_top, - left: all_left - }); - let url6 = new fabric.Group(g6).toDataURL({ - height: 2787, - width: 2787, - top: black_top, - left: black_left - }); - // let white_url2 = canvas2.getObjects()[0].toDataURL({ - // height: 2787, - // width: 2787, - // }); - //console.log(canvas.getObjects()) - let w = document.body.clientWidth * 0.7 + "px"; - let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; - let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; - - if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { - //高度超出,则按高度计算 - h = document.body.clientHeight * 0.8 + "px"; - h1 = document.body.clientHeight * 0.8 - 150 + "px"; - w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 - } - - let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; - if (objs1.length != 0 & objs2.length == 0) { - op.flag = 2; - } else if (objs1.length == 0 & objs2.length != 0) { - op.flag = 3; - } - op.frontColorPic = desEncrypt(url1); - op.backColorPic = ''; - //op.frontBlPic = url5; - //op.backBlPic = url6; - op.frontBlackPic = ''; - op.backBlackPic = ''; - for (let item of _objs1) { - let idx = getIndex(item, canvas1); - if (idx == 0) continue;//background-image - let type = objs1[idx - 1].type; - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 1, - "name": objs1[idx - 1].name, - "FontColor": objs1[idx - 1].black ? "#000000" : "@", - "transparency": item.get("opacity"), - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 3) { - //text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - } else if (type == 11) { - //circle_text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs1[idx - 1].circleCenter - }); - } else if (type == 4) { - //out_text - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 3, - "name": objs1[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "DefaultText": item.get("text"), - "FontBackgroudColor": item.get("textBackgroundColor"), - "FontSize": parseInt(item.get("fontSize")), - "FontColor": item.get("fill"), - "Exceed": objs1[idx - 1].exceed, - "AutoWrap": objs1[idx - 1].autoWrap, - "FixSize": objs1[idx - 1].fixSize, - "Bold": item.get("fontWeight") == "bold", - "Underline": item.get("underline"), - "Italic": item.get("fontStyle") == "italic", - "Strikeout": item.get("linethrough"), - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)), - align: objs1[idx - 1].align - }); - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 4, - "name": objs1[idx - 1].name, - "FontColor": objs1[idx - 1].color, - "DefaultText": objs1[idx - 1].val, - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 9) { - //条形码 - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 5, - "name": objs1[idx - 1].name, - "FontSize": parseInt(objs1[idx - 1].fontSize), - "DefaultText": objs1[idx - 1].val, - "HideText": objs1[idx - 1].show, - "FontFamily": objs1[idx - 1].font, - "FontColor": objs1[idx - 1].color, - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 10) { - let xy = getAbsoluteXY(item); - op.frontData.push({ - "type": 6, - "name": objs1[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "FontSize": parseInt(item.get("fontSize")), - "FontColor": item.get("fill"), - "start": objs1[idx - 1].start, - "add": parseInt(objs1[idx - 1].add), - "x": parseInt((xy[0] - background_image1.left).toFixed(0)), - "y": parseInt((xy[1] - background_image1.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } - } - //obj2 - for (let item of _objs2) { - let idx = getIndex(item, canvas2); - if (idx == 0) continue;//background-image - let type = objs2[idx - 1].type; - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 1, - "name": objs2[idx - 1].name, - "FontColor": objs2[idx - 1].black ? "#000000" : "@", - "transparency": item.get("opacity"), - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 3) { - //text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.backData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - } else if (type == 11) { - //circle_text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.backData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs2[idx - 1].circleCenter - }); - } else if (type == 4) { - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 3, - "name": objs2[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "DefaultText": item.get("text"), - "FontSize": parseInt(item.get("fontSize")), - "FontBackgroudColor": item.get("textBackgroundColor"), - "FontColor": item.get("fill"), - "Exceed": objs2[idx - 1].exceed, - "AutoWrap": objs2[idx - 1].autoWrap, - "FixSize": objs2[idx - 1].fixSize, - "Bold": item.get("fontWeight") == "bold", - "Underline": item.get("underline"), - "Italic": item.get("fontStyle") == "italic", - "Strikeout": item.get("linethrough"), - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)), - align: objs2[idx - 1].align - }); - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 4, - "name": objs2[idx - 1].name, - "FontColor": objs2[idx - 1].color, - "DefaultText": objs2[idx - 1].val, - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 9) { - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 5, - "name": objs2[idx - 1].name, - "DefaultText": objs2[idx - 1].val, - "HideText": objs1[idx - 1].show, - "FontSize": parseInt(objs2[idx - 1].fontSize), - "FontFamily": objs2[idx - 1].font, - "FontColor": objs2[idx - 1].color, - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } else if (type == 10) { - let xy = getAbsoluteXY(item); - op.backData.push({ - "type": 6, - "name": objs2[idx - 1].name, - "FontFamily": item.get("fontFamily"), - "FontSize": parseInt(item.get("fontSize")), - "FontColor": item.get("fill"), - "start": parseInt(objs2[idx - 1].start), - "add": parseInt(objs2[idx - 1].add), - "x": parseInt((xy[0] - background_image2.left).toFixed(0)), - "y": parseInt((xy[1] - background_image2.top).toFixed(0)), - "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate": parseInt(item.get("angle").toFixed(0)) - }); - } - } - dialog.showSaveDialog({ - title: language_str("saveFile"),//'保存文件' - filters: [ - { name: 'CS File Type', extensions: ['cs'] }, - ] - }).then(result => { - if (result.canceled) return; - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.cs'; - } - let fs = require('fs') - op.soonType = 1 - fs.writeFileSync(result.filePath, JSON.stringify(op)) - }).catch(err => { - console.log() - }) - //return false 开启该代码可禁止点击该按钮关闭 - }) - }) - }); - var _keyctrl = false, _keyc = false, _textEdit = false, _inputEdit = false; - $("input").focus(function () { - console.log(1) - _inputEdit = true - }) - $("input").blur(function () { - _inputEdit = false - }) - $("textarea").focus(function () { - console.log(1) - _inputEdit = true - }) - $("textarea").blur(function () { - _inputEdit = false - }) - $("body").keyup(function (event) { - // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) - if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 - _keyctrl = false; - }; - if (event.keyCode == 67) { - _keyc = false; - }; - }) - $("body").keydown(function (event) { - // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) - const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 - - // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) - if (!_inputEdit && !_textEdit) { - // CTRL/CMD+N - 新建 - if (isModifierKey && event.keyCode == 78) { - event.preventDefault(); - $("#new").click(); - return; - } - // CTRL/CMD+O - 打开 - if (isModifierKey && event.keyCode == 79) { - event.preventDefault(); - $("#open").click(); - return; - } - // CTRL/CMD+S - 保存 - if (isModifierKey && !event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - $("#save").click(); - return; - } - // CTRL/CMD+SHIFT+S - 保存副本 - if (isModifierKey && event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - output(null, saveAs); - return; - } - // CTRL/CMD+Z - 还原 - if (isModifierKey && !event.shiftKey && event.keyCode == 90) { - event.preventDefault(); - $("#previous").click(); - return; - } - // CTRL/CMD+Y - 重做 - if (isModifierKey && !event.shiftKey && event.keyCode == 89) { - event.preventDefault(); - $("#next").click(); - return; - } - // CTRL/CMD+C - 复制 - if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { - event.preventDefault(); - copySelection(); - return; - } - // CTRL/CMD+V - 粘贴 - if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { - event.preventDefault(); - pasteFromClipboard(); - return; - } - - // CTRL/CMD + SHIFT + DEL (Windows) 或 CMD + SHIFT + Backspace (Mac) - 清空 - if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { - event.preventDefault(); - $("#clean").click(); - return; - } - - // CTRL/CMD + DEL (Windows) 或 CMD + Backspace (Mac) - 删除 - if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { - event.preventDefault(); - $("#delete").click(); - return; - } - - } - // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) - if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) - _keyctrl = true; - if (_keyc && _textEdit) { - //同时按下且在编辑状态 - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - //clipboard.writeText((clipboard.readText()+' ')) - } - } - if (event.keyCode == 67) { - _keyc = true; - if (_keyctrl & _textEdit) { - //同时按下且在编辑状态 - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - //clipboard.writeText((clipboard.readText()+' ')) - } - } - if (event.keyCode == 37) { - //left - let v = -1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5; - } - let x = canvas.getActiveObject().get("left"); - x += v; - canvas.getActiveObject().set("left", x); - canvas.renderAll(); - recordState(); - updateControls(); - } - if (event.keyCode == 38) { - //up - let v = -1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5; - } - let y = canvas.getActiveObject().get("top"); - y += v; - canvas.getActiveObject().set("top", y); - canvas.renderAll(); - recordState(); - updateControls(); - } - if (event.keyCode == 39) { - //right - let v = 1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5; - } - let x = canvas.getActiveObject().get("left"); - x += v; - canvas.getActiveObject().set("left", x); - canvas.renderAll(); - recordState(); - updateControls(); - } - if (event.keyCode == 40) { - //down - let v = +1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5; - } - let y = canvas.getActiveObject().get("top"); - y += v; - canvas.getActiveObject().set("top", y); - canvas.renderAll(); - recordState(); - updateControls(); - } - // 普通删除键(DEL键,没有按CTRL/CMD) - if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { - let actObjs = canvas.getActiveObjects(); - let allObjs = canvas.getObjects(); - let flag = 0; - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 - objs.splice(i - 1, 1); - canvas.remove(item); - flag = 1; - - } - } - updateList(); - canvas.discardActiveObject(); - if (flag == 1) { - recordState(); - canvas.renderAll(); - handleObjectSelected() - } - } - }); - let pre_add_image; - $("#addPic").click(function () { - OpenDialog(); - function OpenDialog() { - dialog.showOpenDialog({ - title: language_str("selectPic"),//请选择图片 - buttonLabel: language_str("comf"),//"确认" - filters: [ - { name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }, - ] - }).then(result => { - if (result.canceled) return; - //console.log(result.filePaths) - fabric.Image.fromURL(result.filePaths[0], function (image) { - //image.left = pointer.x; - //image.top = pointer.y; - image.setSrc(image.toDataURL(), function (image) { - pre_add_image = image; - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 1; - }); - //canvas.add(image); - //objs.push({type: 1}); - //canvas.renderAll(); - //updateList(); - //recordState(); - }); - }).catch(err => { - console.log(err) - }) - } - }); - $("#addOutPic").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 2; - }); - $("#addText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 3; - }); - - $("#addCircleText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 11; - }); - $("#addOutText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 4; - }); - $("#addRect").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 5; - }); - $("#addCircle").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 6; - }); - $("#addLine").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 7; - }); - $("#addQrCode").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 8; - }); - $("#addBarCode").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 9; - }); - $("#addCounter").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - addState = 10; - }); - $("#version_change").click(function () { - if (bg_version == 1) { - bg_version = 2; - } else { - bg_version = 1; - } - // 类型改变 - $("#source_front").attr("src", "./public/images/front_bg" + bg_version + "_1.png"); - canvas.renderAll(); - - }); - $("#front_side").click(function () { - $(this).addClass("ui-button-active"); - $("#back_side").removeClass("ui-button-active"); - $(".canvas-container:eq(0)").show(); - $(".canvas-container:eq(1)").hide(); - console.log("------"); - console.log(recordObjs1); - console.log("------"); - canvas = canvas1; - objs = objs1; - background_image = background_image1; - next_objs = next_objs1; - pre_objs = pre_objs1; - next_json = next_json1; - pre_json = pre_json1; - recordObjs = recordObjs1; - recordJson = recordJson1; - step = step1; - updateList(); - - $("#line_control").hide(); - $("#base_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//背景 - }); - $("#back_side").click(function () { - $(this).addClass("ui-button-active"); - $("#front_side").removeClass("ui-button-active"); - $(".canvas-container:eq(0)").hide(); - $(".canvas-container:eq(1)").show(); - canvas = canvas2; - objs = objs2; - background_image = background_image2; - next_objs = next_objs2; - pre_objs = pre_objs2; - next_json = next_json2; - pre_json = pre_json2; - recordObjs = recordObjs2; - recordJson = recordJson2; - step = step2; - updateList(); - $("#line_control").hide(); - $("#base_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//"背景" - }); - $("#top_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一个 - _objs[0].set("top", background_image.get("top")); - canvas.renderAll(); - return; - } - let top = 0; - for (let item of _objs) { - let t_top = item.get("top"); - if (t_top < top) { - top = t_top; - } - } - groupItems = []; - for (let item of _objs) { - item.set("top", top); - groupItems.push(item); - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - - }); - $("#bottom_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get("strokeWidth") - } - _objs[0].set("top", background_image.get("top") + background_image.get("height") - _objs[0].get("height") * _objs[0].get("scaleY") - other); - canvas.renderAll(); - return; - } - let bottom = 0; - for (let item of _objs) { - let idx = getIndex(item); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - let t_b = item.get("top") + item.get("height") * item.get("scaleY") + other; - if (t_b > bottom) { - bottom = t_b; - } - } - groupItems = [] - for (let item of _objs) { - let idx = getIndex(item); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - item.set("top", bottom - item.get("height") * item.get("scaleY") - other); - groupItems.push(item) - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - - }); - $("#center_y_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get("strokeWidth") - } - _objs[0].set("top", background_image.get("top") + (background_image.get("height") - (_objs[0].get("height") + other) * _objs[0].get("scaleY")) / 2); - canvas.renderAll(); - return; - } - let top = 0; - for (let item of _objs) { - let t_top = item.get("top"); - if (t_top < top) { - top = t_top; - } - } - groupItems = [] - for (let item of _objs) { - let idx = getIndex(item); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - item.set("top", top + (item.group.height - (item.get("height") + other) * item.get("scaleY")) / 2); - groupItems.push(item) - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - }); - $("#left_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一个 - _objs[0].set("left", background_image.get("left")); - canvas.renderAll(); - return; - } - let left = 0; - for (let item of _objs) { - let t_left = item.get("left"); - if (t_left < left) { - left = t_left; - } - } - groupItems = [] - for (let item of _objs) { - item.set("left", left); - groupItems.push(item) - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - }); - $("#right_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - _objs[0].set("left", background_image.get("left") + background_image.get("width") - _objs[0].get("width") * _objs[0].get("scaleX") - other); - canvas.renderAll(); - return; - } - let right = 0; - for (let item of _objs) { - let idx = getIndex(item); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - let t_r = item.get("left") + item.get("width") * item.get("scaleX") + other; - if (t_r > right) { - right = t_r; - } - } - let groupItems = [] - - for (let item of _objs) { - let idx = getIndex(item); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - item.set("left", right - item.get("width") * item.get("scaleX") - other); - groupItems.push(item) - } - //BUG1 - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - }); - $("#center_x_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get("strokeWidth") - } - _objs[0].set("left", background_image.get("left") + (background_image.get("width") - (_objs[0].get("width") + other) * _objs[0].get("scaleX")) / 2); - canvas.renderAll(); - return; - } - let left = 0; - for (let item of _objs) { - let t_left = item.get("left"); - if (t_left < left) { - left = t_left; - } - } - groupItems = [] - for (let item of _objs) { - let idx = getIndex(item); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - item.set("left", left + (item.group.width - (item.get("width") + other) * item.get("scaleX")) / 2); - groupItems.push(item) - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - }); - $("#up_obj").click(function () {//上移,放到前面 数组中在后面 - let idx = getIndex(canvas.getActiveObject()); - if (idx + 1 != canvas.getObjects().length) { - canvas.bringForward(canvas.getActiveObject()); - canvas.renderAll(); - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx]; - objs[idx] = temp; - //console.log(objs); - //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; - - recordState(); - updateList(); - selectObj(idx); - } - }); - $("#down_obj").click(function () {//下移,放到后面 数组中在前面 - let idx = getIndex(canvas.getActiveObject()); - if (idx != 1) { - canvas.sendBackwards(canvas.getActiveObject()); - canvas.renderAll(); - //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; - - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx - 2]; - objs[idx - 2] = temp; - recordState(); - updateList(); - selectObj(idx - 2); - } - }); - $("#set_bg").click(function () {//设置为背景 - let obj = canvas.getActiveObject(); - console.log(obj, obj.get("selectable")) - let idx = getIndex(obj); - const scaleX = 2787 / obj.width - const scaleY = 2787 / obj.height - obj.set({ - scaleX: scaleX, - scaleY: scaleY, - left: ($('#canvas1').width() - 2787) / 2, - top: ($('#canvas1').height() - 2787) / 2, - }); - $('#text_w').val(2787) - $('#text_h').val(2787) - $('#text_x').val(0) - $('#text_y').val(0) - if (obj.get("selectable")) { - obj.set("selectable", false); - $("#lock_img").attr("src", "public/images/lock20.png") - } - canvas.sendToBack(obj); - canvas.bringForward(obj); - canvas.renderAll(); - let objItem = objs.splice(idx - 1, 1)[0] - objs.unshift(objItem) - recordState(); - updateList(); - selectObj(0); - }); - - // ========================================== - // 【新增】状态恢复后的回调函数 - // 作用:确保图片加载完毕后,再刷新界面,防止白屏或对象丢失 - // ========================================== - function onStateRestored() { - // 1. 重新获取背景图引用 (因为旧对象被销毁了) - // 注意:canvas 是全局变量,指向当前激活的画板 - if (canvas.getObjects().length > 0) { - background_image = canvas.getObjects()[0]; - background_image.selectable = false; // 确保背景不可选 - background_image.evented = false; - } - - // 2. 渲染画布 - canvas.requestRenderAll(); - - // 3. 取消选中状态 - selectObj(-1); - - // 4. 隐藏所有属性面板 - $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); - - // 5. 刷新左侧列表 - updateList(); - } - // ========================================== - // 撤销操作 (修复版) - // ========================================== - $("#previous").click(function () { - if (step.val == 0) return; - - step.val--; - - // 判断当前是正面还是背面 - if ($("#front_side").hasClass("ui-button-active")) { - // 1. 恢复业务数据 - objs1 = JSON.parse(recordObjs1[step.val]); - objs = objs1; - - // 2. 恢复画布 (使用回调函数!) - canvas.loadFromJSON(recordJson1[step.val], function() { - onStateRestored(); // 图片加载完才执行这一步 - }); - } else { - // 背面逻辑 - objs2 = JSON.parse(recordObjs2[step.val]); - objs = objs2; - - canvas.loadFromJSON(recordJson2[step.val], function() { - onStateRestored(); - }); - } - }); - // ========================================== - // 重做操作 (修复版) - // ========================================== - $("#next").click(function () { - // 获取当前历史记录的最大长度 - let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; - - if (step.val >= maxLen - 1) return; - - step.val++; - - if ($("#front_side").hasClass("ui-button-active")) { - objs1 = JSON.parse(recordObjs1[step.val]); - objs = objs1; - - canvas.loadFromJSON(recordJson1[step.val], function() { - onStateRestored(); // 图片加载完才执行这一步 - }); - } else { - objs2 = JSON.parse(recordObjs2[step.val]); - objs = objs2; - - canvas.loadFromJSON(recordJson2[step.val], function() { - onStateRestored(); - }); - } - }); - - $("#clean").click(function () { - objs.splice(0, objs.length); - - let allObjs = canvas.getObjects(); - for (let item of allObjs) { - if (item != allObjs[0]) { - canvas.remove(item); - } - } - updateList(); - recordState(); - }); - $("#rotate").click(function () { - let obj = canvas.getActiveObject(); - obj.rotate(Math.round((obj.get("angle") + 90) % 360)); - $("#text_r").val(obj.get("angle")); - canvas.renderAll(); - updateControls(); - recordState(); - }); - $('#vertical_obj').click(function () { - let obj = canvas.getActiveObject(); - obj.set("flipY", !obj.get("flipY")); - canvas.renderAll(); - updateControls(); - recordState(); - }); - $('#align_obj').click(function () { - let obj = canvas.getActiveObject(); - obj.set("flipX", !obj.get("flipX")); - canvas.renderAll(); - updateControls(); - recordState(); - }); - $("#text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#text_underline").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("underline", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("underline", true) - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#text_linethrough").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("linethrough", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("linethrough", true) - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - - $("#circle_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#circle_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - - $('#out_left_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_left_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'left') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'left' - recordState() - canvas.renderAll() - }) - $('#out_right_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_right_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'right') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'right' - recordState() - canvas.renderAll() - }) - $('#out_center_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_center_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'center') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'center' - recordState() - canvas.renderAll() - }) - - $("#out_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#out_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#out_text_underline").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("underline", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("underline", true) - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#out_text_linethrough").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("linethrough", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("linethrough", true) - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#lock").click(function () { - canvas.getActiveObject().set("selectable", !canvas.getActiveObject().get("selectable")); - if (canvas.getActiveObject().get("selectable")) { - $("#lock_img").attr("src", "public/images/unlock20.png")//已经上锁,需要解锁 - } else { - $("#lock_img").attr("src", "public/images/lock20.png")//没上锁,可以上锁 - } - recordState(); - canvas.renderAll(); - }); - $("#text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#text_back_color").val("#ffffff"); - canvas.renderAll(); - }); - $("#circle_text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#circle_text_back_color").val("#ffffff"); - canvas.renderAll(); - }); - $("#out_text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#out_text_back_color").val("#ffffff"); - canvas.renderAll(); - }); - $("#rect_transparent").click(function () { - canvas.getActiveObject().set("fill", ""); - $("#rect_color").val("#ffffff"); - canvas.renderAll(); - }); - $("#rect_stroke_transparent").click(function () { - canvas.getActiveObject().set("stroke", ""); - $("#rect_stroke").val("#ffffff"); - canvas.renderAll(); - }); - $("#circle_transparent").click(function () { - canvas.getActiveObject().set("fill", ""); - $("#circle_color").val("#ffffff"); - canvas.renderAll(); - }); - $("#circle_stroke_transparent").click(function () { - canvas.getActiveObject().set("stroke", ""); - $("#circle_stroke").val("#ffffff"); - canvas.renderAll(); - }); - - function saveAs(op1, callback) { - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let _g1 = [], _g2 = []; - let left = 0, top = 0; - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g1.push(fabric.util.object.clone(_o)); - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - left = 0, top = 0; - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g2.push(fabric.util.object.clone(_o)); - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; - let con_o = Object.assign(o, op1); - dialog.showSaveDialog({ - title: language_str("saveFile"),//'保存文件' - filters: [ - { name: 'Soon File Type', extensions: ['soon'] }, - ] - }).then(result => { - if (result.filePath == "") { return; } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon'; - } - let fs = require('fs') - con_o.soonType = 1 - fs.writeFileSync(result.filePath, JSON.stringify(con_o)) - openAs.name = result.filePath; - layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' - saveHistory(); - if (callback && typeof callback === 'function') { - callback(); - } - }).catch(err => { - console.log() - }) - } - function save(op1, callback) { - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let _g1 = [], _g2 = []; - let left = 0, top = 0; - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g1.push(fabric.util.object.clone(_o)); - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - left = 0, top = 0; - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g2.push(fabric.util.object.clone(_o)); - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; - let con_o = Object.assign(o, op1); - if (openAs.name != "") { - //打开的文件 - let fs = require('fs') - con_o.soonType = 1 - fs.writeFileSync(openAs.name, JSON.stringify(con_o)) - layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' - saveHistory(); - if (callback && typeof callback === 'function') { - callback(); - } - return; - } - dialog.showSaveDialog({ - title: language_str("saveFile"),//'保存文件' - filters: [ - { name: 'Soon File Type', extensions: ['soon'] }, - ] - }).then(result => { - if (result.filePath == "") { return; } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon'; - } - let fs = require('fs') - con_o.soonType = 1 - fs.writeFileSync(result.filePath, JSON.stringify(con_o)) - openAs.name = result.filePath; - layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' - saveHistory(); - if (callback && typeof callback === 'function') { - callback(); - } - }).catch(err => { - console.log() - }) - } - - // =========================================================== - // 1. 剪贴板数据定义 (修改:改为存储数据 data,而不是对象引用) - // =========================================================== - let clipboardData = { - data: null, // 存储序列化后的 JSON 数据 + 业务属性 - offset: 10 // 粘贴位置偏移量 - }; - - // =========================================================== - // 2. 复制函数:深度拷贝当前选中对象的数据 - // =========================================================== - function copySelection() { - const activeObjects = canvas.getActiveObjects(); - - if (!activeObjects || activeObjects.length === 0) { - layer.msg(language_str("selectPic") || "请先选择对象"); - return; - } - - // 【核心修复】 - // 遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 - clipboardData.data = activeObjects.map(obj => { - let idx = getIndex(obj); // 获取对象在画布中的索引 - - // 获取业务属性 (objs数组中对应的那一项) - // 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1 - let appData = (idx > 0 && objs[idx-1]) ? objs[idx-1] : { type: 0 }; - - return { - // 1. 保存画面样式 (坐标、颜色、字号等) - fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data']), - // 2. 保存业务属性 (深拷贝,防止引用冲突) - appData: JSON.parse(JSON.stringify(appData)) - }; - }); - - clipboardData.offset = 10; // 重置偏移量 - console.log(`已复制 ${activeObjects.length} 个对象`); - layer.msg("已复制"); - } - - // =========================================================== - // 3. 粘贴函数:根据保存的数据重建对象 - // =========================================================== - function pasteFromClipboard() { - // 检查是否有数据 - if (!clipboardData.data || clipboardData.data.length === 0) { - // layer.msg("剪贴板为空"); - return; - } - - // 1. 提取 Fabric JSON 准备恢复画面 - const jsonToEnliven = clipboardData.data.map(item => item.fabricJson); - - // 2. 恢复对象 (异步过程) - fabric.util.enlivenObjects(jsonToEnliven, function(objects) { - canvas.discardActiveObject(); - - objects.forEach((clone, index) => { - // --- A. 处理画面位置 --- - clone.set({ - left: clone.left + clipboardData.offset, - top: clone.top + clipboardData.offset, - evented: true - }); - - // 如果是组内对象,需要特殊处理坐标 - if (clone.group) { - clone.set({ - left: clone.group.left + clipboardData.offset, - top: clone.group.top + clipboardData.offset - }); - } - - // --- B. 处理业务数据 --- - // 取出复制时保存的 appData - let savedAppData = clipboardData.data[index].appData; - let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝 - - // 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy) - if (newAppData.name) { - newAppData.name = newAppData.name + "_copy"; - } - - // --- C. 添加到画布和数据数组 --- - canvas.add(clone); - objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组 - }); - - // 3. 选中新粘贴出来的对象 - let sel = new fabric.ActiveSelection(objects, { canvas: canvas }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - - // 4. 刷新列表和历史记录 - updateList(); - recordState(); - - // 5. 高亮左侧列表最后一项 (即新粘贴的项) - if(objs.length > 0) selectObj(objs.length - 1); - - // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) - clipboardData.offset += 10; - }); - } - - // =========================================================== - // 4. 按钮事件绑定 (保持原样) - // =========================================================== - // 复制 - $("#copy").click(function () { - copySelection(); - }) - // 粘贴 - $("#paste").click(function () { - pasteFromClipboard(); - }) - - // 保存 (保持你原有的保存逻辑不变) - $("#save").click(function () { - output(); // 这里调用你的 output 函数 - return; - // ... (下面是你原有的被 return 截断的代码,建议以后清理掉,但这里我按要求保留) - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - // ... (省略你原来那段很长的保存代码) ... - }); - - - function getDate() { - let date = new Date(); - return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); - } - function getAbsoluteXY(_obj) { - let coord = _obj.get("lineCoords"); - return [_obj.get("left"), _obj.get("top")] - } - let fullPath = path.join(exePath, 'data.json') - // ========================================== - // 历史记录保存 (修复结构Bug + 自动置顶) - // ========================================== - function saveHistory() { - let fs = require('fs'); - let j = { history: [] }; // 默认结构必须是数组 - - try { - // 1. 读取并解析现有文件 - if (fs.existsSync(fullPath)) { - let content = fs.readFileSync(fullPath).toString(); - // 防止空文件报错 - if (content.trim()) { - j = JSON.parse(content); - } - } - } catch (e) { - console.error("读取历史记录失败,将重置:", e); - // 出错时使用默认空数组,不中断流程 - j = { history: [] }; - } - - // 确保 j.history 是数组 (防御性编程) - if (!Array.isArray(j.history)) { - j.history = []; - } - - // 2. 【核心优化】先过滤掉当前文件(如果已存在),然后加到最前面 - // 这样可以实现“最近使用的文件排在第一位”的效果 - let currentPath = openAs.name; - let newList = j.history.filter(item => item.path !== currentPath); - - // 3. 插入到头部 (Unshift) - newList.unshift({ - time: getDate(), - path: currentPath, - // type: 1 // 如果需要记录文件类型也可以加在这里 - }); - - // 4. (可选) 限制历史记录数量,比如只保留最近20条,防止文件无限膨胀 - if (newList.length > 20) { - newList = newList.slice(0, 20); - } - - j.history = newList; - - // 5. 统一写入 - try { - fs.writeFileSync(fullPath, JSON.stringify(j)); - } catch (e) { - console.error("写入历史记录失败:", e); - } - } - - function open(file) { - console.log(file) - let fs = require('fs') - var fsData = fs.readFileSync(file) - openAs.name = file; - let j = JSON.parse(fsData.toString()); - let left = background_image.left; - let top = background_image.top; - let new_left = j.f.objects[0].left; - let new_top = j.f.objects[0].top; - for (let o of j.f.objects) { - o.left = o.left - new_left + left; - o.top = o.top - new_top + top; - } - // for (let o of j.b.objects) { - // o.left = o.left - new_left + left; - // o.top = o.top - new_top + top; - // } - console.log(j.f) - canvas1.loadFromJSON(j.f); - canvas2.loadFromJSON(j.b); - background_image1 = canvas1.getObjects()[0]; - background_image2 = canvas2.getObjects()[0]; - - objs2 = j.bo; - objs1 = j.fo; - step1.val = 0; - step2.val = 0; - recordJson1 = []; - recordJson2 = []; - recordObjs1 = []; - recordObjs2 = []; - if ($("#front_side").hasClass("ui-button-active")) { - objs = objs1; - recordObjs = recordObjs1; - recordJson = recordJson1; - step = step1; - background_image = background_image1; - } else if ($("#back_side").hasClass("ui-button-active")) { - objs = objs2; - recordObjs = recordObjs2; - recordJson = recordJson2; - background_image = background_image2; - step = step2; - } - canvas1.renderAll(); - canvas2.renderAll(); - updateList(); //修复打开新文件后无法撤销的bug - setTimeout(() => { - recordObjs1.push(JSON.stringify(objs1)) - let j1 = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - j1.objects[0].hoverCursor = "default"; - recordJson1.push(j1); - - recordObjs2.push(JSON.stringify(objs2)) - let j2 = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - // j2.objects[0].hoverCursor = "default"; - recordJson2.push(j2); - }, 0); - } - $("#open").click(function () { - if (step.val == 0) { - OpenDialog(); - return; - } - layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件? - output(OpenDialog); - layer.close(index); - - }, function (index) { - OpenDialog(); - }); - function OpenDialog() { - dialog.showOpenDialog({ - title: "请选择文件",//confirm - buttonLabel: language_str("comf"),//"确认" - filters: [ - { name: 'Soon File Type', extensions: ['soon'] }, - ] - }).then(result => { - if (result.canceled) return; - console.log(result.filePaths) - let fs = require('fs') - var fsData = fs.readFileSync(result.filePaths[0]) - openAs.name = result.filePaths[0]; - let j = JSON.parse(fsData.toString()); - let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; - if (type == 2) { - ipcRenderer.send('open-design-page', openAs.name, type); - return - } - let left = background_image.left; - let top = background_image.top; - let new_left = j.f.objects[0].left; - let new_top = j.f.objects[0].top; - for (let o of j.f.objects) { - o.left = o.left - new_left + left; - o.top = o.top - new_top + top; - } - // for (let o of j.b.objects) { - // o.left = o.left - new_left + left; - // o.top = o.top - new_top + top; - // } - canvas1.loadFromJSON(j.f); - canvas2.loadFromJSON(j.b); - background_image1 = canvas1.getObjects()[0]; - background_image2 = canvas2.getObjects()[0]; - - objs2 = j.bo; - objs1 = j.fo; - step1.val = 0; - step2.val = 0; - recordJson1 = []; - recordJson2 = []; - recordObjs1 = []; - recordObjs2 = []; - if ($("#front_side").hasClass("ui-button-active")) { - objs = objs1; - recordObjs = recordObjs1; - recordJson = recordJson1; - step = step1; - background_image = background_image1; - } else if ($("#back_side").hasClass("ui-button-active")) { - objs = objs2; - recordObjs = recordObjs2; - recordJson = recordJson2; - background_image = background_image2; - step = step2; - } - canvas1.renderAll(); - canvas2.renderAll(); - updateList(); - // 修复打开新文件后无法撤销的bug - setTimeout(() => { - recordObjs1.push(JSON.stringify(objs1)) - let j1 = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - j1.objects[0].hoverCursor = "default"; - recordJson1.push(j1); - - recordObjs2.push(JSON.stringify(objs2)) - let j2 = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - j2.objects[0].hoverCursor = "default"; - recordJson2.push(j2); - }, 0); - }).catch(err => { - console.log(err) - }) - } - }); - function clearAll() { - //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; - //let j = JSON.parse(blank); - //canvas1.setZoom(1); - //canvas2.setZoom(1); - //canvas1.loadFromJSON(j.f); - //canvas2.loadFromJSON(j.b); - let t_objs1 = canvas1.getObjects(); - let t_objs2 = canvas2.getObjects(); - for (let i = 1; i < t_objs1.length; i++) { - canvas1.remove(t_objs1[i]); - } - for (let i = 1; i < t_objs2.length; i++) { - canvas2.remove(t_objs2[i]); - } - objs2 = []; - objs1 = []; - step1.val = 0; - step2.val = 0; - recordJson1 = []; - recordJson2 = []; - recordObjs1 = []; - recordObjs2 = []; - if ($("#front_side").hasClass("ui-button-active")) { - objs = objs1; - recordObjs = recordObjs1; - recordJson = recordJson1; - step = step1; - } else if ($("#back_side").hasClass("ui-button-active")) { - objs = objs2; - recordObjs = recordObjs2; - recordJson = recordJson2; - step = step2; - } - openAs.name = ""; - canvas1.renderAll(); - canvas2.renderAll(); - updateList(); - console.log(); - } - $("#new").click(function () { - if (step.val == 0) { - ipcRenderer.send('open-first-page'); - return; - } - layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件? - output(() => ipcRenderer.send('open-first-page')); - return; - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - let _g1 = [], _g2 = []; - let left = 0, top = 0; - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g1.push(fabric.util.object.clone(_o)); - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - left = 0, top = 0; - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g2.push(fabric.util.object.clone(_o)); - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 2787, - width: 2787, - top: top, - left: left - }); - let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; - if (openAs.name != "") { - //打开的文件 - let fs = require('fs') - o.soonType = 1 - fs.writeFileSync(openAs.name, JSON.stringify(o)) - layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' - /* - 2022-07-16 直接跳转至首页 - */ - ipcRenderer.send('open-first-page'); - //saveHistory(); - //clearAll(); - return; - } - dialog.showSaveDialog({ - title: language_str("saveFile"),//'保存文件' - filters: [ - { name: 'Soon File Type', extensions: ['soon'] }, - ] - }).then(result => { - if (result.filePath == "") { return; } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon'; - } - let fs = require('fs') - o.soonType = 1 - fs.writeFileSync(result.filePath, JSON.stringify(o)) - openAs.name = result.filePath; - layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' - /* - 2022-07-16 直接跳转至首页 - */ - ipcRenderer.send('open-first-page'); - //saveHistory(); - //clearAll(); - }).catch(err => { - console.log() - }) - layer.close(index); - - }, function (index) { - /* - 2022-07-16 直接跳转至首页 - */ - //clearAll(); - ipcRenderer.send('open-first-page'); - }); - }); - function chagneTitle() { - - } - function addBackground() { - // 类型改变 - fabric.Image.fromURL('./public/images/bg_front_1.png', function (image) { - let width = $("#canvas1").width() - - image.left = ($("#canvas1").width() - image.width * image.get("scaleX")) / 2; - image.top = ($("#canvas1").height() - image.height * image.get("scaleY")) / 2; - - image.selectable = false; - image.hoverable = false; - image.hoverCursor = "default" - image.setSrc(image.toDataURL(), function (image) { - image.scale(1, 1); - background_image1 = image; - background_image = background_image1; - canvas.add(image); - canvas.renderAll(); - //pre_objs1.push(JSON.stringify(objs1)); - //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); - - recordObjs1.push(JSON.stringify(objs1)); - recordJson1.push(canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"])); - }); - // 类型改变 - fabric.Image.fromURL('./public/images/bg_back.png', function (image) { - image.left = ($("#canvas2").width() - image.width) / 2; - image.top = ($("#canvas2").height() - image.height) / 2; - - image.selectable = false; - image.hoverable = false; - image.hoverCursor = "default" - - image.setSrc(image.toDataURL(), function (image) { - background_image2 = image; - canvas2.add(image); - canvas2.renderAll(); - //pre_objs2.push(JSON.stringify(objs2)); - //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); - - recordObjs2.push(JSON.stringify(objs2)); - recordJson2.push(canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"])); - - let file = GetFile().get('file') - console.log(file) - if (file && file != 'empty') { - open(file) - } - }); - }); - is_bgi_add = true; - }); - } - function selectObject(target, index) { - updateControls(); - - $("#base_control").show(); - //$("#text_x").val((target.left - background_image.left).toFixed(1)); - //$("#text_y").val((target.top - background_image.top).toFixed(1)); - //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); - //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); - //$("#text_r").val(target.get("angle").toFixed(0)); - } - function updateControls() { - $("#text_r").val(canvas.getActiveObject().get("angle").toFixed(0)); - $("#text_x").val((canvas.getActiveObject().get("left") - background_image.left).toFixed(0)); - $("#text_y").val((canvas.getActiveObject().get("top") - background_image.top).toFixed(0)); - $("#text_h").val((canvas.getActiveObject().get("scaleY") * canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("scaleX") * canvas.getActiveObject().get("width")).toFixed(0)); - if (canvas.getActiveObject().get("selectable")) { - $("#lock_img").attr("src", "public/images/unlock20.png")//已经上锁,需要解锁 - } else { - $("#lock_img").attr("src", "public/images/lock20.png")//没上锁,可以上锁 - } - } - function checkName(name) { - //检查字段名是否重复了 - for (let item of objs1) { - if (item.name == name) { - return false; - } - } - for (let item of objs2) { - if (item.name == name) { - return false; - } - } - return true; - } - function resName(pre_name) { - //返回的是不重复的字段名 - let num = (objs1.length + objs2.length) - 1; - do { - num++; - } - while (!checkName(pre_name + num));//重复了就再加1 - return pre_name + num; - } - function addPic(pointer) { - - pre_add_image.left = pointer.x; - pre_add_image.top = pointer.y; - - canvas.add(pre_add_image); - - objs.push({ type: 1, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - //OpenDialog(); - //function OpenDialog() - //{ - // dialog.showOpenDialog({ - // title: "请选择图片", - // buttonLabel: "确认", - // filters: [ - // { name: 'Image File Type', extensions: ['png', 'jpg'] }, - // ] - // }).then(result => { - // if(result.canceled)return; - // //console.log(result.filePaths) - // fabric.Image.fromURL(result.filePaths[0], function(image) { - // image.left = pointer.x; - // image.top = pointer.y; - // - // canvas.add(image); - // objs.push({type: 1}); - // canvas.renderAll(); - // updateList(); - // recordState(); - // } ); - // }).catch(err => { - // console.log(err) - // }) - //} - } - function selectPic(target, index) { - //透明度 - - $("#pic_control").show(); - $("#set_bg").show(); - pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - - let idx = getIndex(canvas.getActiveObject()); - // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); - - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - - } - function addOutPic(pointer) { - fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { - let width = canvas.getWidth(); - let height = canvas.getHeight(); - image.left = pointer.x; - image.top = pointer.y; - image.opacity = 1; - //image.on("selected",function(){//test well - // console.log(222); - //}); - image.setSrc(image.toDataURL(), function (image) { - canvas.add(image); - objs.push({ type: 2, name: resName("OUT_PIC_"), black: false }); - canvas.renderAll(); - updateList(); - recordState(); - }); - }); - } - function selectOutPic(target, index) { - //字段名 - $("#out_pic_control").show(); - out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - let idx = getIndex(canvas.getActiveObject()); - $("#out_pic_field_name").val(objs[idx - 1].name); - // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); - - - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - } - - function addText(pointer) { - let text = new fabric.Text('TEXT', {//源:文本 - left: pointer.x, - top: pointer.y, - fill: "#000000", - textBackgroundColor: "", - fontStyle: "normal", - fontWeight: "normal", - fontSize: 100, - underline: false, - linethrough: false, - fontFamily: 'Arial' - }); - text.setControlVisible("ml", false); - text.setControlVisible("mt", false); - text.setControlVisible("mr", false); - text.setControlVisible("mb", false); - canvas.add(text); - objs.push({ type: 3, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectText() { - //console.log(canvas.getActiveObject().get("fontFamily")); - $("#text_control").show(); - $("#text_color").val(canvas.getActiveObject().get("fill")); - $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); - let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#text_size").val(val); - $("#text_text").val(canvas.getActiveObject().get("text")); - $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); - - if (canvas.getActiveObject().get("underline") == true) { - $("#text_underline").addClass("ui-radio-acitve") - } else { - $("#text_underline").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontWeight") == "bold") { - $("#text_bold").addClass("ui-radio-acitve") - } else { - $("#text_bold").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontStyle") == "italic") { - $("#text_italics").addClass("ui-radio-acitve") - } else { - $("#text_italics").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("linethrough") == true) { - $("#text_linethrough").addClass("ui-radio-acitve") - } else { - $("#text_linethrough").removeClass("ui-radio-acitve") - } - $("#circle_text_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - - } - function getDisdance(x1, y1, x2, y2) { - var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 - var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 - var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 - return distance; - } - function getDeg(pointer) { - // 计算点击位置与画布中心的坐标差 - var centerX = canvas.width / 2; - var centerY = canvas.height / 2; - var mouseX = pointer.x; - var mouseY = pointer.y; - var dx = mouseX - centerX; - var dy = mouseY - centerY; - // 计算旋转角度(弧度) - var angleRad = Math.atan2(dy, dx); - // 将角度转换为度数 - var angleDeg = angleRad * (180/Math.PI); - if (angleDeg < 0) { - angleDeg += 360; - } - angleDeg += 90 - return angleDeg - } - function addCircleText(pointer) { - let distance = getDisdance(pointer.x, pointer.y, canvas.width/2,canvas.height/2) - let angleDeg = getDeg(pointer) - let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 - left: pointer.x, - top: pointer.y, - fill: "#000000", - textBackgroundColor: "", - fontStyle: "normal", - fontWeight: "normal", - diameter: distance*2, - fontSize: 100, - underline: false, - linethrough: false, - lockScalingX: true, - lockScalingY: true, - fontFamily: 'Arial', - originX: 'center', - // originY: 'center', - angle: angleDeg, - flipped: pointer.y > canvas.height/2 ? true : false - }); - // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) - // text.set({diameter: distance*2}) - text.setControlVisible("ml", false); - text.setControlVisible("mt", false); - text.setControlVisible("mr", false); - text.setControlVisible("mb", false); - canvas.add(text); - objs.push({ type: 11, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectCircleText() { - let idx = getIndex(canvas.getActiveObject()); - $("#circle_text_control").show(); - $("#circle_text_color").val(canvas.getActiveObject().get("fill")); - $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); - circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); - circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")-500); - $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); - $("#circle_text_text").val(canvas.getActiveObject().get("text")); - $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); - - if (canvas.getActiveObject().get("underline") == true) { - $("#circle_text_underline").addClass("ui-radio-acitve") - } else { - $("#circle_text_underline").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontWeight") == "bold") { - $("#circle_text_bold").addClass("ui-radio-acitve") - } else { - $("#circle_text_bold").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontStyle") == "italic") { - $("#circle_text_italics").addClass("ui-radio-acitve") - } else { - $("#circle_text_italics").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("linethrough") == true) { - $("#circle_text_linethrough").addClass("ui-radio-acitve") - } else { - $("#circle_text_linethrough").removeClass("ui-radio-acitve") - } - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#rotate").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - - } - function addOutText(pointer) { - //MERGE_TEXT_1 - let text = new fabric.Text("MERGE TEXT", {//源:合成文本 - left: pointer.x, - top: pointer.y, - fill: "#000000", - fontStyle: "normal", - fontWeight: "normal", - fontSize: 100, - textBackgroundColor: "", - underline: false, - linethrough: false, - fontFamily: 'Arial', - textAlign: 'center' - }); - - //text.on({ - // 'scaling': function(e) { - // var obj = this, - // w = obj.width * obj.scaleX, - // h = obj.height * obj.scaleY; - // - // obj.set({ - // 'height' : h, - // 'width' : w, - // 'scaleX' : 1, - // 'scaleY' : 1 - // }); - // } - //}); - text.setControlVisible("ml", false); - text.setControlVisible("mt", false); - text.setControlVisible("mr", false); - text.setControlVisible("mb", false); - - canvas.add(text); - objs.push({ type: 4, name: resName("OUT_TEXT_"), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectOutText() { - //color - //size - const align = canvas.getActiveObject().get('textAlign') - if (align === 'left') { - $('#out_left_alignment').parent().addClass('tool1') - } else if (align === 'right') { - $('#out_right_alignment').parent().addClass('tool1') - } else { - $('#out_center_alignment').parent().addClass('tool1') - } - $("#out_text_control").show(); - $("#out_text_color").val(canvas.getActiveObject().get("fill")); - $("#out_text_text").val(canvas.getActiveObject().get("text")); - let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#out_text_size").val(val); - $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); - let idx = getIndex(canvas.getActiveObject()); - $("#out_text_field_name").val(objs[idx - 1].name); - $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); - $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); - $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); - $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); - if (canvas.getActiveObject().get("underline") == true) { - $("#out_text_underline").addClass("ui-radio-acitve") - } else { - $("#out_text_underline").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontWeight") == "bold") { - $("#out_text_bold").addClass("ui-radio-acitve") - } else { - $("#out_text_bold").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontStyle") == "italic") { - $("#out_text_italics").addClass("ui-radio-acitve") - } else { - $("#out_text_italics").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("linethrough") == true) { - $("#out_text_linethrough").addClass("ui-radio-acitve") - } else { - $("#out_text_linethrough").removeClass("ui-radio-acitve") - } - - - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - } - function addRect(pointer) { - let rect = new fabric.Rect({ - left: pointer.x, - top: pointer.y, - fill: '', - stroke: "#ff0000", - strokeWidth: 10, - width: 300, - height: 300 - }); - canvas.add(rect); - objs.push({ type: 5, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectRect() { - //color - $("#rect_control").show(); - $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); - $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); - $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); - $("#rect_radius").val(canvas.getActiveObject().get("rx")); - rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - let a = canvas.getActiveObject().getCoords(); - console.log(getCoordsMinX(a)); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - } - function addCircle(pointer) { - let circle = new fabric.Circle({ - left: pointer.x, - top: pointer.y, - fill: '', - stroke: '#ff0000', - strokeWidth: 10, - radius: 150, - strokeUniform: true, - }); - canvas.add(circle); - objs.push({ type: 6, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectCircle() { - //border-color - //color - $("#circle_control").show(); - $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); - $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); - $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); - circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - } - function addLine(pointer) { - let line = new fabric.Line([100, 200, 400, 200], { - left: pointer.x, - top: pointer.y, - stroke: '#ff0000', - strokeWidth: 10, - strokeDashArray: [0, 0], - }); - - line.setControlVisible("tl", false); - line.setControlVisible("tr", false); - line.setControlVisible("br", false); - line.setControlVisible("bl", false); - line.setControlVisible("mt", false); - line.setControlVisible("mb", false); - canvas.add(line); - objs.push({ type: 7, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectLine() { - //color - //weight - $("#line_control").show(); - - let idx = getIndex(canvas.getActiveObject()); - $("#line_color").val(canvas.getActiveObject().get("stroke")); - $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); - $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) - - - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - } - function addQrCode(pointer) { - - let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }); - fabric.Image.fromURL(qr, function (image) { - image.left = pointer.x; - image.top = pointer.y; - image.scale(2.4, 2.4) - canvas.add(image); - objs.push({ type: 8, name: resName("QR_CODE_"), color: "#000000", val: "https://www.cardsoon.com", black: false }); - canvas.renderAll(); - updateList(); - recordState(); - }); - } - function selectQrCode() { - //字段名 - //background-color - //color - $("#qrcode_Controll").show(); - let idx = getIndex(canvas.getActiveObject()); - $("#qrcode_field_name").val(objs[idx - 1].name); - $("#qrcode_color").val(objs[idx - 1].color); - $("#qrcode_val").val(objs[idx - 1].val); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#barcode_controll").hide(); - } - function addBarCode(pointer) { - JsBarcode("#barcode", "123456789", { margin: 0, lineColor: "#000000", fontSize: 18, font: "Arial" }); - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - fabric.Image.fromURL(bar, function (image) { - image.left = pointer.x; - image.top = pointer.y; - image.scale(2.4, 2.4) - canvas.add(image); - objs.push({ type: 9, name: resName("BAR_CODE_"), val: "123456789", color: "#000000", fontSize: 18, font: "Arial", show: false, black: false });//show是取消文字显示 - canvas.renderAll(); - updateList(); - recordState(); - }); - } - function selectBarCode() { - //字段名 - //background-color - //color - $("#barcode_controll").show(); - let idx = getIndex(canvas.getActiveObject()); - $("#barcode_field_name").val(objs[idx - 1].name); - $("#barcode_font_family").val(objs[idx - 1].font); - let val = parseFloat(objs[idx - 1].fontSize)*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#barcode_size").val(val); - $("#barcode_val").val(objs[idx - 1].val); - $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); - $("#line_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - } - function addCounter(pointer) { - let text = new fabric.Text('0', { - left: pointer.x, - top: pointer.y, - fontSize: 100, - fill: "#000000", - }); - canvas.add(text); - objs.push({ type: 10, name: resName("COUNTER_"), start: 0, add: 1, black: false }); - canvas.renderAll(); - updateList(); - recordState(); - console.log(canvas.getObjects()) - } - function selectCounter() { - //起始 - //终止 - //增量 - $("#counter_control").show(); - - - let idx = getIndex(canvas.getActiveObject()); - $("#counter_start").val(objs[idx - 1].start); - $("#counter_add").val(objs[idx - 1].add); - $("#counter_color").val(canvas.getActiveObject().get("fill")); - $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); - let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#count_size").val(val); - - $("#line_control").hide(); - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - - } - function getIndex(target, _canvas = null) { - let temp_objs; - if (_canvas == null) { - temp_objs = canvas.getObjects(); - } else { - temp_objs = _canvas.getObjects(); - } - - let i = 0; - for (let res of temp_objs) { - if (target == res) { - return i; - } - i++; - } - return 0; - } - function getType(target) { - let temp_objs = canvas.getObjects(); - let i = 0; - for (let res of temp_objs) { - if (target == res) { - return objs[i].type; - } - i++; - } - } - // =========================================================== - // 1. 刷新对象列表 (序号顺序:底层=1,顶层=N,修复:Flex布局防遮挡 + 序号靠右) - // =========================================================== - function updateList() { - let str = ""; - let i = 0; - - // 定义行样式:Flex布局,两端对齐,相对定位防止溢出 - const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; - // 定义左侧内容样式 - const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; - // 定义序号样式:靠右,有左边框 - const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; - - for (let item of objs) { - let displayIndex = i + 1; // 序号底层为1 - - // 图标逻辑 - let iconName = "rect.svg"; - switch (item.type) { - case 1: iconName = "images.svg"; break; - case 2: iconName = "images_add.svg"; break; - case 3: iconName = "text.svg"; break; - case 4: iconName = "text2.svg"; break; - case 5: iconName = "rect.svg"; break; - case 6: iconName = "round.svg"; break; - case 7: iconName = "line.svg"; break; - case 8: iconName = "qrcode.svg"; break; - case 9: iconName = "barcode.svg"; break; - case 10: iconName = "counter.svg"; break; - case 11: iconName = "text3.svg"; break; - } - - // 名称逻辑 - let nameText = item.name ? item.name : - (item.type === 3 ? "TEXT_" + displayIndex : - (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); - - // 组装 HTML - str = `
-
- - ${nameText} -
- ${displayIndex} -
` + str; - i++; - } - $(".obj_list").html(str); - - // 重新绑定点击事件(防止 updateList 后点击失效) - $(".obj_list > div").on("click", function () { - let imgId = $(this).find("img").attr("id"); - if(imgId) { - let idx = parseInt(imgId.replace("obj", "")); - selectObj(idx); - } - }); - } - - // =========================================================== - // 2. 列表选中高亮 (修复:自动滚动 + 防冲突) - // =========================================================== - function selectObj(objIndices, isFromCanvas = false) { - let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); - - // 清除所有高亮 - $('.obj_list > div').css("background-color", "transparent"); - - // 遍历列表查找并高亮 - $('.obj_list > div').each(function () { - let $img = $(this).find("img"); - if ($img.length === 0) return; - - let idIndex = parseInt($img.attr("id").replace("obj", "")); - - if (indicesArray.includes(idIndex)) { - $(this).css("background-color", "#6F7A84"); - - // 自动滚动到可视区域 - if (isFromCanvas && indicesArray[0] === idIndex) { - this.scrollIntoView({ behavior: "smooth", block: "nearest" }); - } - } - }); - - // 如果是点击列表,触发画布选中 - if (!isFromCanvas && indicesArray.length === 1) { - let target = canvas.getObjects()[indicesArray[0] + 1]; - if (target) { - canvas.setActiveObject(target); - canvas.requestRenderAll(); - handleObjectSelected(); - } - } - } - function recordState() { - //pre_objs.push(JSON.stringify(objs)); - //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - //next_json = []; - //next_objs = []; - //recordObjs.push(JSON.stringify(objs)); - //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - ++step.val; - for (let i = step.val; i < recordObjs.length; i++) { - recordObjs.pop(); - recordJson.pop(); - } - //recordObjs.splice(step.val, 1, JSON.stringify(objs)) - //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) - recordObjs.push(JSON.stringify(objs)) - j = canvas.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - j.objects[0].hoverCursor = "default"; - recordJson.push(j); - } - function getCoordsMinX(acoords) { - x = acoords[0].x; - for (let item of acoords) { - if (item.x < x) { - x = item.x - } - } - return x; - } - function getCoordsMaxX(acoords) { - x = acoords[0].x; - for (let item of acoords) { - if (item.x > x) { - x = item.x - } - } - return x; - - } - function getCoordsMinY(acoords) { - y = acoords[0].y; - for (let item of acoords) { - if (item.y < y) { - y = item.y - } - } - return y; - - } - function getCoordsMaxY(acoords) { - y = acoords[0].y; - for (let item of acoords) { - if (item.y > y) { - y = item.y - } - } - return y; - } - - $("#text_y").bind('input propertychange', function () { - if ($("#text_y").val() == "") { - $("#text_y").val(0) - } - let val = background_image.top; - if ($("#text_y").val() != "") { - val = background_image.top + parseInt($("#text_y").val()); - } - canvas.getActiveObject().set("top", val) - canvas.renderAll(); - recordState(); - }) - $("#text_x").bind('input propertychange', function () { - if ($("#text_x").val() == "") { - $("#text_x").val(0) - } - let val = background_image.left; - if ($("#text_x").val() != "") { - val = background_image.left + parseInt($("#text_x").val()); - } - canvas.getActiveObject().set("left", val) - canvas.renderAll(); - recordState(); - }) - $("#text_w").bind('input propertychange', function () { - let val = "0"; - if ($("#text_w").val() != "") { - val = $("#text_w").val(); - } - let type = objs[getIndex(canvas.getActiveObject()) - 1].type; - if (type == 3 || type == 4) { - canvas.getActiveObject().set("width", parseInt(val)) - } else { - canvas.getActiveObject().set("scaleX", parseInt(val) / canvas.getActiveObject().get("width")) - } - - canvas.renderAll(); - recordState(); - }) - $("#text_h").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - if (objs[idx - 1].type == 7) { - if ($("#text_h").val() == "") { - $("#text_h").val(canvas.getActiveObject().get("strokeWidth")); - } - canvas.getActiveObject().set("strokeWidth", parseInt($("#text_h").val())); - canvas.renderAll(); - return; - } - let val = "0"; - if ($("#text_h").val() != "") { - val = $("#text_h").val(); - } - let type = objs[getIndex(canvas.getActiveObject()) - 1].type; - if (type == 3 || type == 4) { - canvas.getActiveObject().set("height", parseInt(val)) - } else { - canvas.getActiveObject().set("scaleY", parseInt(val) / canvas.getActiveObject().get("height")) - } - - canvas.renderAll(); - recordState(); - }) - $("#text_r").bind('input propertychange', function () { - - if ($("#text_r").val() == "") { - $("#text_r").val("0") - } - $("#text_r").val(parseInt($("#text_r").val()) % 360) - //canvas.getActiveObject().set("rotate", val) - canvas.getActiveObject().rotate(parseInt($("#text_r").val())) - canvas.renderAll(); - recordState(); - }) - $("#out_pic_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#out_pic_field_name").val(); - updateList(); - recordState(); - }) - - $("#text_black").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].black = true; - } else { - objs[idx - 1].black = false; - } - recordState(); - }); - $("#out_text_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#out_text_field_name").val(); - updateList(); - recordState(); - }) - $("#qrcode_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#qrcode_field_name").val(); - updateList(); - recordState(); - }) - $("#qrcode_color").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let qr = jrQrcode.getQrBase64($("#qrcode_val").val(), { padding: 0, foreground: $(this).val() }); - let idx = getIndex(item); - objs[idx - 1].color = $(this).val(); - item.setSrc(qr, function (img) { - canvas.renderAll(); - recordState(); - }); - }); - $("#qrcode_val").bind('change', function () { - let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' - let item = canvas.getActiveObject(); - let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); - let idx = getIndex(item); - objs[idx - 1].val = val; - item.setSrc(qr, function (img) { - canvas.renderAll(); - recordState(); - }); - }); - $("#barcode_color").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].color = $(this).val(); - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - }); - $("#barcode_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#barcode_field_name").val(); - updateList(); - recordState(); - }) - $("#text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#text_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#text_back_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#text_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#text_size").val())) { - val = parseFloat($("#text_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#text_size").val(val) - canvas.getActiveObject().set("fontSize", val*dpi/72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - recordState(); - }) - $("#circle_text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#circle_text_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#circle_text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); - - canvas.renderAll(); - recordState(); - }) - $("#circle_text_text").bind('input propertychange', function () { - let val = $("#circle_text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); - canvas.renderAll(); - updateList(); - recordState(); - }) - $("#out_text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#out_text_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#out_text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#out_text_back_color").val()); - - canvas.renderAll(); - recordState(); - }) - $("#out_text_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#out_text_size").val())) { - val = parseFloat($("#out_text_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#out_text_size").val(val) - canvas.getActiveObject().set("fontSize", val*dpi/72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - recordState(); - }) - // 监听合成文本输入框 (占位符) - $("#out_text_text").on('input propertychange', function () { - let val = $(this).val(); - - // 【核心修改】如果值为空,强制给 2 个空格 - // 这样宽度足够宽,容易点击选中 - if (val === "") { - val = " "; - } - - let obj = canvas.getActiveObject(); - if (obj) { - obj.set("text", val); - obj.setCoords(); - canvas.requestRenderAll(); - updateList(); - recordState(); - } - }); - $("#rect_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#rect_color").val()); - - canvas.renderAll(); - recordState(); - }) - $("#rect_stroke").bind('input propertychange', function () { - canvas.getActiveObject().set("stroke", $("#rect_stroke").val()); - - canvas.renderAll(); - recordState(); - }) - $("#rect_stroke_width").bind('input propertychange', function () { - if ($("#rect_stroke_width").val() == "") { - $("#rect_stroke_width").val("0") - } else { - $("#rect_stroke_width").val(parseInt($("#rect_stroke_width").val())) - } - canvas.getActiveObject().set("strokeWidth", parseInt($("#rect_stroke_width").val())); - canvas.renderAll(); - recordState(); - }) - $("#rect_radius").bind('input propertychange', function () { - if ($("#rect_radius").val() == "") { - $("#rect_radius").val("0") - } else { - $("#rect_radius").val(parseInt($("#rect_radius").val())) - } - canvas.getActiveObject().set("rx", parseInt($("#rect_radius").val())); - canvas.getActiveObject().set("ry", parseInt($("#rect_radius").val())); - canvas.renderAll(); - recordState(); - }) - $("#circle_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#circle_color").val()); - - canvas.renderAll(); - recordState(); - }) - $("#circle_stroke").bind('input propertychange', function () { - canvas.getActiveObject().set("stroke", $("#circle_stroke").val()); - - canvas.renderAll(); - recordState(); - }) - $("#circle_stroke_width").bind('input propertychange', function () { - if ($("#circle_stroke_width").val() == "") { - $("#circle_stroke_width").val("0") - } else { - $(this).val(parseInt($(this).val())) - } - canvas.getActiveObject().set("strokeWidth", parseInt($("#circle_stroke_width").val())); - canvas.renderAll(); - recordState(); - }) - $("#counter_start").bind('input propertychange', function () { - canvas.getActiveObject().set("text", $("#counter_start").val()); - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].start = $(this).val(); - canvas.renderAll(); - recordState(); - }) - $("#counter_add").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].add = $(this).val(); - canvas.renderAll(); - recordState(); - }) - $("#counter_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#counter_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#count_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#count_size").val())) { - val = parseFloat($("#count_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#count_size").val(val) - canvas.getActiveObject().set("fontSize", val*dpi/72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - recordState(); - }) - $("#line_color").bind('input propertychange', function () { - canvas.getActiveObject().set("stroke", $("#line_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#line_dist").bind('input propertychange', function () { - if ($("#line_dist").val() == "") { - $("#line_dist").val(0) - } - canvas.getActiveObject().set("strokeDashArray", [parseInt($("#line_dist").val()), parseInt($("#line_dist").val())]) - canvas.renderAll(); - recordState(); - }) - // 监听普通文本输入 (占位符) - $("#text_text").on('input propertychange', function () { - // 1. 获取输入框的值,不要用 trim(),否则无法输入空格 - let val = $(this).val(); - - // 2. 核心修改:只有当删光时,才给 4 个空格作为占位符 - if (val === "") { - val = " "; // 4个空格,比原来的2个更宽,更好点 - } - // 3. 更新对象 - // 注意:这里直接用 val,不再用 ` ${val} `,这样有文字时就不会强制加空格了 - canvas.getActiveObject().set("text", val); - canvas.requestRenderAll(); // 使用 requestRenderAll 性能更好 - updateList(); - recordState(); - }); - $("#barcode_val").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].val = $(this).val(); - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - }) - $("#barcode_show").click(function () { - let item = canvas.getActiveObject(); - let idx = getIndex(canvas.getActiveObject()); - let val = $("#barcode_size").val() - if ($(this).prop("checked")) { - objs[idx - 1].show = true; - val = 0 - } else { - objs[idx - 1].show = false; - } - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val*dpi/72, font: objs[idx - 1].font }); - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - }); - $("#barcode_size").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - let val = 1 - if (parseFloat($(this).val())) { - val = parseFloat($(this).val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#barcode_size").val(val) - objs[idx - 1].fontSize = val*dpi/72; - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - }) - $('#count_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - recordState(); - }); - $('#text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - recordState(); - }); - - $('#circle_text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - recordState(); - }); - - $('#language_select').on('change', function () {//语言选择 - langua_ge($(this).find('option:selected').val()); - s_lan = $(this).find('option:selected').val(); - localStorage.setItem("lang", s_lan); - }); - $('#barcode_font_family').on('change', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].font = $(this).find('option:selected').text(); - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - //recordState(); - }); - $('#out_text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - recordState(); - }); - $(".obj_list").on("click", "div", function () { - let idx = objs.length - 1 - $(this).index(); - selectObj(idx); - }); - $("#exceed").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].exceed = true; - } else { - objs[idx - 1].exceed = false; - } - recordState(); - }); - $("#autoWrap").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - - objs[idx - 1].autoWrap = true; - } else { - objs[idx - 1].autoWrap = false; - } - recordState(); - }); - $("#fixSize").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - - objs[idx - 1].fixSize = true; - } else { - objs[idx - 1].fixSize = false; - } - recordState(); - }); - $("#circle_text_center").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].circleCenter = 1; - } else { - objs[idx - 1].circleCenter = 0; - } - recordState(); - }); - $("#delete").click(function () { - let actObjs = canvas.getActiveObjects(); - let allObjs = canvas.getObjects(); - let flag = 0; - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 - objs.splice(i - 1, 1); - canvas.remove(item); - flag = 1; - - } - } - updateList(); - canvas.discardActiveObject(); - if (flag == 1) { - recordState(); - canvas.renderAll(); - } - // let actObjs = canvas.getActiveObjects(); - // let allObjs = canvas.getObjects(); - // let flag = 0; - // for( let item of actObjs){ - // if ( item != allObjs[0]){ - // let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 - // objs.splice(i-1,1); - // canvas.remove(item); - // flag = 1; - // updateList(); - // } - // } - // selectObj(-1); - // if(flag == 1){ - // recordState(); - // } - }); - - //在关于我们页面,获取版本号。 - // 注意:function 前面加了 async 关键字 - $("#about").click(async function () { - // 1. 先异步获取版本号 - let version = "0.0.0"; // 默认值,防止获取失败 - try { - if (window.sysAPI && window.sysAPI.getAppVersion) { - const v = await window.sysAPI.getAppVersion(); - version = "v" + v; // 拼接一个 v,变成 v2.3.017 - } - } catch (e) { - console.error("获取版本号失败", e); - } - - // 2. 获取成功后再弹出窗口 - layer.open({ - type: 1 - , title: language_str("about") - , area: '450px;' - , id: 'LAY_layuipro' - , moveType: 1 - , content: '
' + - '
' + - '
Soon Design
' + - // 【修改点】这里使用了刚才获取到的 version 变量 - '
' + language_str("vers") + ' ' + version + '
' + - '
' + language_str("copyright") + '
' + - '
' - }); - }) - $("#help").click(() => { - ipcRenderer.send('open-help-file'); - }); - - - // =========================================================== - // 对象选中处理 - // =========================================================== - function handleObjectSelected() { - let select_obj = canvas.getActiveObject(); - - // 1. 未选中任何对象 - if (!select_obj) { - $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); - $("#component_type").text(language_str("bg")); - selectObj(-1, true); // 传 true,只更新UI - return; - } - - // 2. 获取索引 - let i = getIndex(select_obj); - - if (i != 0) { - // --- 选中了普通对象 --- - --i; // 转换为 objs 数组下标 - - if(objs[i]) { - switch (objs[i].type) { - case 1: // 图片 - $("#component_type").text(language_str("img")); - selectObject(select_obj, i); selectPic(select_obj, i); break; - case 2: // 合成图片 - $("#component_type").text(language_str("simg")); - selectObject(select_obj, i); selectOutPic(select_obj, i); break; - case 3: // 文本 - $("#component_type").text(language_str("text")); - selectObject(select_obj, i); selectText(select_obj, i); break; - case 4: // 合成文本 - $("#component_type").text(language_str("stext")); - selectObject(select_obj, i); selectOutText(select_obj, i); break; - case 5: // 矩形 - $("#component_type").text(language_str("rect")); - selectObject(select_obj, i); selectRect(select_obj, i); break; - case 6: // 圆形 - $("#component_type").text(language_str("circ")); - selectObject(select_obj, i); selectCircle(select_obj, i); break; - case 7: // 直线 - $("#component_type").text(language_str("line")); - selectObject(select_obj, i); selectLine(select_obj, i); break; - case 8: // 二维码 - $("#component_type").text(language_str("qrc")); - selectObject(select_obj, i); selectQrCode(select_obj, i); break; - case 9: // 条形码 - $("#component_type").text(language_str("barc")); - selectObject(select_obj, i); selectBarCode(select_obj, i); break; - case 10: // 计数器 - $("#component_type").text(language_str("counter")); - selectObject(select_obj, i); selectCounter(select_obj, i); break; - case 11: // 圆形文本 - $("#component_type").text(language_str("ctext")); - selectObject(select_obj, i); selectCircleText(select_obj, i); break; - } - } - - // 【关键】这里传入 true! - // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 - selectObj(i, true); - - } else { - // --- 选中了背景 --- - $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); - selectObj(-1, true); - $("#component_type").text(language_str("bg")); - } - } - - // =========================================================== - // 统一事件监听 (替代原有的 canvas1.on 和 canvas2.on) - // =========================================================== - // 提取对象移动逻辑 (供正反面复用) - function handleObjectMoving(e) { - let obj = e.target; - let idx = getIndex(obj); - // 圆形文本特殊处理 - if (idx > 0 && idx <= objs.length && objs[idx - 1] && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { - let pointer = { x: obj.left, y: obj.top }; - let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); - let angleDeg = getDeg(pointer); - let flipped = pointer.y > canvas.height / 2 ? true : false; - - if(typeof circle_text_diameter !== 'undefined') circle_text_diameter.setValue(distance * 2 - 500); - obj.set("diameter", distance * 2); - obj.set("angle", angleDeg); - obj.set("flipped", flipped); - } - updateControls(); - } - - // 启动监听 - // =========================================================== - // 统一事件绑定 (已集成边界检测) - // =========================================================== -// =========================================================== - // 3. 统一事件绑定 (修复:使用 Global Composite Operation 实现完美遮罩) - // =========================================================== - function initCanvasEvents() { - [canvas1, canvas2].forEach(currentCanvas => { - - // 1. 鼠标移动 - currentCanvas.on("mouse:move", (e) => { - let offsetX = 0, offsetY = 0; - if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { - offsetX = background_image.left; - offsetY = background_image.top; - } - if(e.pointer) { - $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); - } - updateRulerGuides(e.e); - }); - - // 2. 鼠标移出 - currentCanvas.on("mouse:out", () => { - $("#ruler-guide-h").hide(); $("#ruler-guide-v").hide(); - }); - - // 3. 选中处理 - currentCanvas.on('mouse:down', handleObjectSelected); - - // 4. 鼠标松开 (添加对象) - currentCanvas.on("mouse:up", (e) => { - if (addState === 0) return; - const ptr = e.pointer; - switch (addState) { - case 1: addPic(ptr); break; - case 2: addOutPic(ptr); break; - case 3: addText(ptr); break; - case 4: addOutText(ptr); break; - case 5: addRect(ptr); break; - case 6: addCircle(ptr); break; - case 7: addLine(ptr); break; - case 8: addQrCode(ptr); break; - case 9: addBarCode(ptr); break; - case 10: addCounter(ptr); break; - case 11: addCircleText(ptr); break; - } - addState = 0; - if(typeof background_image !== 'undefined') background_image.hoverCursor = "default"; - currentCanvas.renderAll(); - $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); - }); - - // 5. 对象移动 (只保留圆形文本逻辑,去掉了 checkOutOfBounds) - currentCanvas.on("object:moving", (e) => { - handleObjectMoving(e); - }); - - // 6. 变换事件 - currentCanvas.on("object:rotating", updateControls); - currentCanvas.on("object:scaling", updateControls); - currentCanvas.on("object:rotated", () => { recordState(); }); - currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); - currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); - currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); - - // 7. 对象修改后 - currentCanvas.on('object:modified', function (event) { - if (currentCanvas.getActiveObjects().length > 1) return; - let idx = getIndex(event.target); - if(idx <= 0) return; - let objData = objs[idx - 1]; - let target = event.target; - - if ([3, 4, 10].includes(objData.type)) { - target.fontSize = (target.fontSize * target.scaleX).toFixed(0); - target.scaleX = 1; target.scaleY = 1; - target._clearCache(); - if(objData.type == 3) $("#text_size").val(target.fontSize); - if(objData.type == 4) $("#out_text_size").val(target.fontSize); - if(objData.type == 10) $("#count_size").val(target.fontSize); - currentCanvas.renderAll(); - updateControls(); - } else if (objData.type == 5) { - target.width *= target.scaleX; target.height *= target.scaleY; - target.scaleX = 1; target.scaleY = 1; - updateControls(); - } - recordState(); - }); - // ================================================== - // 8. 【完美修复版】渲染后绘制:外部蒙版 + 中心圆孔 (修复三角形Bug) - // ================================================== - currentCanvas.on('after:render', function () { - let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2; - - if (bgObj) { - const ctx = currentCanvas.getContext(); - const width = currentCanvas.width; - const height = currentCanvas.height; - - // 1. 计算圆心 - const cx = bgObj.left + (bgObj.width * bgObj.scaleX) / 2; - const cy = bgObj.top + (bgObj.height * bgObj.scaleY) / 2; - - // 2. 计算半径 - // 外半径 (光盘大小) - const r_outer = (bgObj.width * bgObj.scaleX) / 2; - - // 内半径 (中心孔) - const dpi = 600; - const mmToPx = dpi / 25.4; - let holeDiameterMm = 24; // 默认 15mm - - // 切换版本判断 (15mm vs 38mm) - if (currentCanvas === canvas1 && typeof bg_version !== 'undefined' && bg_version === 2) { - holeDiameterMm = 42; - } - - // 比例计算法:确保孔径随缩放适配 - // r_inner = r_outer * (孔径 / 光盘标准直径120mm) - const r_inner = r_outer * (holeDiameterMm / 120); - - ctx.save(); - ctx.beginPath(); - - // A. 绘制全屏矩形 (顺时针) - ctx.rect(0, 0, width, height); - - // B. 挖掉外圆 (逆时针) - // 【关键修复】画圆前,必须把笔移动到圆的起点 (cx + r, cy) - // 否则 Canvas 会画一条线连过来,形成奇怪的三角形 - ctx.moveTo(cx + r_outer, cy); - ctx.arc(cx, cy, r_outer, 0, Math.PI * 2, true); - - // C. 填补内圆 (顺时针) - // 【关键修复】同理,把笔移动到内圆的起点 - ctx.moveTo(cx + r_inner, cy); - ctx.arc(cx, cy, r_inner, 0, Math.PI * 2, false); - - ctx.closePath(); - ctx.fillStyle = "rgba(0, 0, 0, 0.9)"; // 90% 遮罩 - ctx.fill(); - ctx.restore(); - } - }); - }); - } - // 立即启动 - initCanvasEvents(); - // =========================================================== - // Slider 滑块初始化 (保持原样) - // =========================================================== - var circle_text_size = slider.render({ - elem: '#circle_text_size', - min: 10, max: 120, - change: function (value) { - canvas.getActiveObject().set("fontSize", value); - canvas.renderAll(); - } - }); - var circle_text_diameter = slider.render({ - elem: '#circle_text_diameter', - min: 500, max: 4000, - change: function (value) { - canvas.getActiveObject().set("diameter", value); - canvas.renderAll(); - } - }); - var out_pic_op = slider.render({ - elem: '#out_pic_op', - change: function (value) { - canvas.getActiveObject().set('opacity', (100 - value) / 100); - canvas.renderAll(); - } - }); - var pic_op = slider.render({ - elem: '#pic_op', - change: function (value) { - canvas.getActiveObject().set("opacity", (100 - value) / 100); - canvas.renderAll(); - } - }); - var rect_op = slider.render({ - elem: '#rect_op', - change: function (value) { - canvas.getActiveObject().set("opacity", (100 - value) / 100); - canvas.renderAll(); - } - }); - var circle_op = slider.render({ - elem: '#circle_op', - change: function (value) { - canvas.getActiveObject().set("opacity", (100 - value) / 100); - canvas.renderAll(); - } - }); -// =========================================================== - // 3. 标尺功能 (0点复位 + 拖拽辅助线 + 透明度) - // =========================================================== - function initRulers() { - const dpi = 600; - const mmToPx = dpi / 25.4; - const rulerHeight = 20; - const rulerWidth = 20; - - const canvasWrapper = $('.canvas-wrapper'); - const canvasBox = $('.canvas_box'); - - // 确保容器定位 - if(canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); - - // 初始化尺寸 - let canvasWidth = canvasWrapper.width() || 800; - let canvasHeight = canvasWrapper.height() || 600; - - const $rulerH = $('#ruler-h'); - const $rulerV = $('#ruler-v'); - - $rulerH.attr('width', canvasWidth).attr('height', rulerHeight); - $rulerV.attr('width', rulerWidth).attr('height', canvasHeight); - - // --- 1. 绑定拖拽事件 (核心新功能) --- - // 定义临时拖拽线 DOM - let $dragLine = null; - - // 通用拖拽开始函数 - const startDragGuide = (type, e) => { - e.preventDefault(); - e.stopPropagation(); - - // 创建临时显示的线 - $dragLine = $('
'); - $('.canvas-wrapper').append($dragLine); - - if (type === 'horizontal') { - // 从上往下拖 (水平线) - $dragLine.css({ - left: 0, width: '100%', height: '1px', top: e.offsetY - }); - } else { - // 从左往右拖 (垂直线) - $dragLine.css({ - top: 0, height: '100%', width: '1px', left: e.offsetX - }); - } - - // 绑定 document 移动事件 - $(document).on('mousemove.ruler', function(moveEvent) { - let wrapperOffset = canvasWrapper.offset(); - if (type === 'horizontal') { - let relY = moveEvent.pageY - wrapperOffset.top; - $dragLine.css('top', relY + 'px'); - } else { - let relX = moveEvent.pageX - wrapperOffset.left; - $dragLine.css('left', relX + 'px'); - } - }); - - // 绑定松开事件 (创建真实的 Fabric 线) - $(document).on('mouseup.ruler', function(upEvent) { - $(document).off('mousemove.ruler mouseup.ruler'); - - if (!$dragLine) return; - - // 获取最终位置 - let wrapperOffset = canvasWrapper.offset(); - let finalX = upEvent.pageX - wrapperOffset.left; - let finalY = upEvent.pageY - wrapperOffset.top; - - // 移除临时线 - $dragLine.remove(); - $dragLine = null; - - // 只有拖入画布区域才创建 - if (finalX > rulerWidth && finalY > rulerHeight) { - createFabricGuide(type, finalX, finalY); - } - }); - }; - - // 绑定到标尺 DOM - $rulerH.off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); - $rulerV.off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); - - // --- 2. 在画布中创建辅助线 --- - function createFabricGuide(type, domX, domY) { - // 计算画布内的逻辑坐标 (减去 margin 偏移,除以缩放) - // 注意:这里我们重新回到“原点在画布0,0”的逻辑 - // canvasBox.offset() 是画布容器的绝对位置 - let boxOffset = canvasBox.offset(); - let wrapperOffset = canvasWrapper.offset(); - - // 计算相对于 canvasBox 的像素位置 - let relX = domX - (boxOffset.left - wrapperOffset.left); - let relY = domY - (boxOffset.top - wrapperOffset.top); - - // 转为 Fabric 坐标 (除以 zoom) - let canvasX = relX / zoom; - let canvasY = relY / zoom; - - // 构造线对象 - let linePoints = []; - if (type === 'horizontal') { - // 水平线:贯穿整个画布宽度 - // 为了让线足够长,我们设一个很大的值,或者设为 canvas.width - linePoints = [-5000, canvasY, 5000, canvasY]; - } else { - // 垂直线 - linePoints = [canvasX, -5000, canvasX, 5000]; - } - - let guideLine = new fabric.Line(linePoints, { - stroke: '#00FFFF', // 青色高亮 - strokeWidth: 1 / zoom, // 线条粗细随缩放调整,保持视觉一致 - strokeDashArray: [5, 5], // 虚线 - selectable: true, // 允许选中移动 - evented: true, - hasControls: false, // 没有缩放控制点 - hasBorders: false, - lockRotation: true, // 锁定旋转 - lockScalingX: true, - lockScalingY: true, - hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', - // 自定义属性标记 - isGuideLine: true, - ignoreSave: true // 保存时可以根据这个标记过滤掉 - }); - - // 限制移动方向 - guideLine.on('moving', function(e) { - if (type === 'horizontal') { - this.left = -5000; // 锁定 X 轴不许动 - } else { - this.top = -5000; // 锁定 Y 轴不许动 - } - }); - - canvas.add(guideLine); - canvas.setActiveObject(guideLine); - canvas.renderAll(); - } - - - // --- 3. 绘制标尺 (核心绘制循环) --- - function updateAndDraw() { - const pxPerMm = zoom * mmToPx; - - // 【修改点】原点复位逻辑 - // 现在原点就是 Canvas 的 (0,0) 点 - // 加上 canvasBox 的 margin 偏移量,让标尺 0 刻度对齐画布边缘 - let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; - let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; - - let originX = boxMarginLeft; - let originY = boxMarginTop; - - const ctxH = document.getElementById('ruler-h').getContext('2d'); - const ctxV = document.getElementById('ruler-v').getContext('2d'); - - const drawAxis = (ctx, isH) => { - let len = isH ? canvasWidth : canvasHeight; - ctx.clearRect(0, 0, isH ? len : rulerWidth, isH ? rulerHeight : len); - - // 背景改为透明 (配合 CSS) - // ctx.fillStyle = 'rgba(44, 44, 44, 0.7)'; // 也可以在这里画 - // ctx.fillRect(...) - // 这里我们留空,让 CSS 背景色透出来 - - ctx.strokeStyle = '#999'; // 刻度颜色稍亮一点 - ctx.lineWidth = 1; - ctx.beginPath(); - - // 绘制底边线 - if(isH) { ctx.moveTo(0, 19); ctx.lineTo(len, 19); } - else { ctx.moveTo(19, 0); ctx.lineTo(19, len); } - ctx.stroke(); - - ctx.fillStyle = '#CCC'; - ctx.font = '9px Arial'; - ctx.textAlign = isH ? 'left' : 'center'; - ctx.textBaseline = isH ? 'top' : 'middle'; - ctx.beginPath(); - - // 绘制刻度 (只向正方向绘制) - // 既然原点复位到0了,我们只需要从0开始画 - for (let mm = 0; ; mm += 1) { - let pos = (isH ? originX : originY) + (mm * pxPerMm); - - if (pos > len) break; - - let isMajor = mm % 10 === 0; // 1cm - let isMedium = mm % 5 === 0; // 0.5cm - let tickLen = isMajor ? 15 : (isMedium ? 8 : 4); - - if(isH) { - ctx.moveTo(pos, 20 - tickLen); ctx.lineTo(pos, 20); - if(isMajor) ctx.fillText(mm/10, pos + 2, 2); // 显示 cm 数值 - } else { - ctx.moveTo(20 - tickLen, pos); ctx.lineTo(20, pos); - if(isMajor) { - ctx.save(); - ctx.translate(8, pos + 2); - ctx.rotate(-Math.PI/2); - ctx.fillText(mm/10, 0, 0); - ctx.restore(); - } - } - } - ctx.stroke(); - }; - - drawAxis(ctxH, true); - drawAxis(ctxV, false); - } - - canvas1.on('after:render', updateAndDraw); - canvas2.on('after:render', updateAndDraw); - $(window).resize(() => setTimeout(updateAndDraw, 100)); - updateAndDraw(); - } - - // 辅助函数:更新光标位置 - function updateRulerGuides(nativeEvent) { - if (!nativeEvent) return; - const wrapper = $('.canvas-wrapper'); - const offset = wrapper.offset(); - if (!offset) return; - $("#ruler-guide-h").css({ left: (nativeEvent.pageX - offset.left) + 'px', display: 'block' }); - $("#ruler-guide-v").css({ top: (nativeEvent.pageY - offset.top) + 'px', display: 'block' }); - } - - - - - }) + + function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + var APIFMS; + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + } catch (err) { + console.log('des 加密 -------------------------'); + console.log(err); + } + return APIFMS; + } + (function (fabric) { + fabric.CurvedText = fabric.util.createClass(fabric.Object, { + type: 'curved-text', + diameter: 0, + kerning: 0, + text: '', + flipped: false, + fill: '#000', + fontFamily: 'Times New Roman', + fontSize: 24, // in px + fontWeight: 'normal', + fontStyle: '', // "normal", "italic" or "oblique". + cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), + strokeStyle: null, + strokeWidth: 0, + initialize: function (text, options) { + options || (options = {}); + if (typeof text === 'object' && text) { + for (let key in text) { + this[key] = text[key] + } + } else { + this.text = text; + } + this.callSuper('initialize', options); + this.set('lockUniScaling', true); + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + }, + _getFontDeclaration: function () { + return [ + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + cropCanvas: function (canvas) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + pix = { x: [], y: [] }, n, + imageData = ctx.getImageData(0, 0, w, h), + fn = function (a, b) { return a - b }; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + if (imageData.data[((y * w + x) * 4) + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // 如果没有找到任何非透明像素,直接返回,不进行裁剪 + if (pix.x.length === 0 || pix.y.length === 0) { + return; + } + pix.x.sort(fn); + pix.y.sort(fn); + n = pix.x.length - 1; + w = pix.x[n] - pix.x[0]; + h = pix.y[n] - pix.y[0]; + // 确保宽度和高度是有效的正数 + if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { + return; + } + var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); + canvas.width = w; + canvas.height = h; + ctx.putImageData(cut, 0, 0); + }, + getCircularText: function () { + var text = this.text, + diameter = this.diameter, + flipped = this.flipped, + kerning = this.kerning, + fill = this.fill, + inwardFacing = true, + startAngle = 0, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'), + cw, // character-width + x, // iterator + clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise + if (flipped) { + // startAngle = 180; + inwardFacing = false; + } + startAngle *= Math.PI / 180; // convert to radians + var d = document.createElement('div'); + d.style.fontFamily = this.fontFamily; + d.style.fontSize = this.fontSize + 'px'; + d.style.fontWeight = this.fontWeight; + d.style.fontStyle = this.fontStyle; + d.textContent = text; + document.body.appendChild(d); + var textHeight = d.offsetHeight; + document.body.removeChild(d); + canvas.width = canvas.height = diameter; + ctx.font = this._getFontDeclaration(); + if (inwardFacing) { text = text.split('').reverse().join('') }; + ctx.translate(diameter / 2, diameter / 2); // Move to center + startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward + ctx.textBaseline = 'middle'; // Ensure we draw in exact center + ctx.textAlign = 'center'; // Ensure we draw in exact center + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; + startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; + } + ctx.rotate(startAngle); + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; // half letter + ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); + if (this.strokeStyle && this.strokeWidth) { + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.miterLimit = 2; + ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + } + ctx.fillStyle = fill; + ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter + } + return canvas; + }, + _set: function (key, value) { + switch (key) { + case 'scaleX': + this.fontSize *= value; + this.diameter *= value; + this.width *= value; + this.scaleX = 1; + if (this.width < 1) { this.width = 1; } + break; + case 'scaleY': + this.height *= value; + this.scaleY = 1; + if (this.height < 1) { this.height = 1; } + break; + default: + this.callSuper('_set', key, value); + break; + } + }, + _render: function (ctx) { + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); + this.setCoords(); + }, + toObject: function (propertiesToInclude) { + return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); + } + }); + fabric.CurvedText.fromObject = function (object, callback, forceAsync) { + return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); + }; + })(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); + + const remote = require('@electron/remote') + var path = require('path'); + // 使用用户数据目录,避免写入需要管理员权限 + const exePath = remote.app.getPath('userData') + const { ipcRenderer } = require('electron') + const { dialog } = require('@electron/remote') + var jrQrcode = require('jr-qrcode'); + var JsBarcode = require('jsbarcode'); + ipcRenderer.send('get-sys-fonts'); + ipcRenderer.send('get-scale-rate'); + const { clipboard } = require('electron') + const dpi = 600 + // // 内部剪贴板 + // let clipboardData = { + // objects: null, + // offset: 10 // 粘贴位置偏移量 + // }; + + layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { + let myDate = new Date(); + let s_lan = ""; + ipcRenderer.send('get-sys-language'); + /* if(myDate.getFullYear()>2021 && myDate.getMonth() >= 1 && myDate.getDate() > 15){ + + }*/ + function GetFile() { + const params = new URLSearchParams(window.location.search); + return params + } + function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)); + }) + + $("[language='t']").each(function (i) { + $(this).attr("title", $(this).attr(lan)); + }) + } + function language_str(str) { + let t = { + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, + "save": { zh: "保存", ozh: "保存", en: "Save" }, + "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, + "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, + "display": { zh: "预览", ozh: "預覽", en: "Display" }, + "output": { zh: "导出", ozh: "導出", en: "Export" }, + "bg": { zh: "背景", ozh: "背景", en: "Background" }, + "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, + "selectPic": { zh: "请选择文件", ozh: "請選擇文件", en: "Please select a file" }, + "img": { zh: "圖片", ozh: "圖片", en: "Image" }, + "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, + "text": { zh: "文本", ozh: "文本", en: "Text" }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, + "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, + "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, + "line": { zh: "直線", ozh: "直線", en: "Line" }, + "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, + "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, + "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, + "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, + "vers": { zh: "版本", ozh: "版本", en: "Version" }, + "comf": { zh: "确认", ozh: "確認", en: "Select" }, + "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + } + return t[str][s_lan]; + } + + var $ = layui.$; + var layer = layui.layer + , slider = layui.slider, colorpicker = layui.colorpicker; + let addState = 0;//0是不添加 + let background_image, background_image1, background_image2; + let bg_version = 1;//front_bg1.png front_bg2.png + var zoom = 1; + $(function () { + // 定义CurvedText类 + + + $("#object_attribute").css("height", "470px"); + $("#obj_list").css("height", document.body.clientHeight - 520); + $(".obj_list").css("height", $("#obj_list").height() - 32); + + $(".main").show(); + $(".canvas_box").css("height", document.body.clientHeight - 100); + zoom = $(".canvas_bg_img").width() / 2787 + $("#canvas1").attr("width", $(".canvas_box").width() / zoom); + $("#canvas1").attr("height", $(".canvas_box").height() / zoom); + $("#canvas2").attr("width", $(".canvas_box").width() / zoom); + $("#canvas2").attr("height", $(".canvas_box").height() / zoom); + var openAs = { + _name: "", + set name(val) { + if (val == "") { + $("title").html('Soon Design'); + } else { + $("title").html('Soon Design - ' + val); + } + this._name = val; + }, + get name() { + return this._name + }, + };//被打开的文件名 + let objs1 = []; + let objs2 = []; + let step1 = { val: 0 }, step2 = { val: 0 }; + let step = step1; + let objs = objs1; + let recordJson1 = [], recordJson2 = [], recordJson = recordJson1, recordObjs1 = [], recordObjs2 = [], recordObjs = recordObjs1; + let pre_objs1 = [], pre_objs2 = [], next_objs1 = [], next_objs2 = [], pre_objs = pre_objs1, next_objs = next_objs1; + let pre_json1 = [], pre_json2 = [], next_json1 = [], next_json2 = [], pre_json = pre_json1, next_json = next_json1; + var canvas1 = new fabric.Canvas('canvas1'); + var canvas2 = new fabric.Canvas('canvas2'); + fabric.Object.prototype.objectCaching = false; + var ctx1 = canvas1.getSelectionContext(); + var ctx2 = canvas2.getSelectionContext(); + let is_bgi_add = false; + canvas1.zoomToPoint(new fabric.Point(0, 0), 1); + canvas2.zoomToPoint(new fabric.Point(0, 0), 1); + + // 初始化标尺 + initRulers(); + + $("#source_front").width(zoom * $("#source_front").width()); + $("#source_back").width(zoom * $("#source_back").width()); + $(".canvas-container:eq(0)").css('transform', 'scale(' + zoom + ')') + $(".canvas-container:eq(1)").css('transform', 'scale(' + zoom + ')') + $(".canvas-container:eq(0)").css('width', 0) + + $(".canvas-container:eq(1)").hide(); + var canvas = canvas1; + fabric.Object.prototype.set({ + borderColor: "#9013FE", + cornerColor: "#9013FE", //激活状态角落图标的填充颜色 + cornerStrokeColor: "#9013FE", //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 6, + cornerSize: 24, + cornerStyle: "circle", //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0, + }); + canvas1.preserveObjectStacking = true; + canvas2.preserveObjectStacking = true; + + + var front_list = []; + ipcRenderer.on('font-list', (event, data) => { + const newFontList = []; + data.map(item => { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')); + } else { + newFontList.push(item); + } + }) + front_list = newFontList; + for (let item of front_list) { + $("#text_font_family").append(""); + $("#circle_text_font_family").append(""); + $("#out_text_font_family").append(""); + $("#barcode_font_family").append(""); + $("#count_font_family").append(""); + } + }); + ipcRenderer.on('sys-lan', (event, data) => { + let lang = localStorage.getItem("lang"); + if (lang) { + s_lan = lang; + langua_ge(lang); + $("#language_select").val(lang); + return; + } + switch (data) { + case 'zh-CN': + s_lan = "zh"; + langua_ge("zh"); + $("#language_select").val("zh"); + localStorage.setItem("lang", "zh"); + break; + case 'zh': + s_lan = "zh"; + langua_ge("zh"); + $("#language_select").val("zh"); + localStorage.setItem("lang", "zh"); + break; + case 'zh-TW': + s_lan = "ozh"; + langua_ge("ozh"); + $("#language_select").val("ozh"); + localStorage.setItem("lang", "ozh"); + break; + case 'en': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-AU': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-CA': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-GB': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-NZ': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-US': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-ZA': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + } + }); + ipcRenderer.on('close', (event, message) => { + layer.confirm('' + language_str("whetherSave") + '', {//是否保存当前文件? + btn: [language_str("save"), language_str("noSave"), language_str("cancel")] //['保存', '不保存', '取消'] + , btn3: function (index, layero) { + + } + }, function (index, layero) { + + output(() => ipcRenderer.send('run-close')); + return; + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + if (openAs.name != "") { + //打开的文件 + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + + ipcRenderer.send('run-close'); + return; + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { ipcRenderer.send('run-close'); return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//保存成功至 + saveHistory(); + ipcRenderer.send('run-close'); + }).catch(err => { + console.log() + }) + layer.close(index); + }, function (index) { + ipcRenderer.send('run-close'); + }); + + }) + addBackground();//添加背景 白色卡片 + + initAligningGuidelines(canvas1); + initAligningGuidelines(canvas2); + //canvas.setZoom(0.5) + //recordObjs1.push(JSON.stringify(objs1)); + //recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //recordObjs2.push(JSON.stringify(objs2)); + //recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + canvas.renderAll();//重新渲染画布 + var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + console.log(val, zoomSlider) + canvas.zoomToPoint( + new fabric.Point(0, 0), + val / 100 + ); + // zoom = val / 100; + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } + + }); + window.rotate = function (id) { + let angle = $(`#${id}`).attr("angle") - 0; + angle += 90; + $(`#${id}`).css("transform", `rotate(${angle}deg)`); + $(`#${id}`).attr("angle", angle); + } + $('#display').on('click', function () { + // 类型改变 + fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + + fabric.Image.fromURL('./public/images/back_bg_1.png', function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + fabric.Image.fromURL('./public/images/op_1.png', function (i3) { + i2.left = background_image.left; + i2.top = background_image.top; + let img3 = i3; + display_func(img1, img2, img3); + }) + }) + }) + }); + // 导出文件 + async function saveImageAsPNG(buffer) { + let fs = require('fs'); + // 弹出保存对话框 + const { canceled, filePath } = await dialog.showSaveDialog({ + title: '保存图片', + defaultPath: path.join(exePath, 'output.png'), // 默认路径 + filters: [{ name: 'PNG Images', extensions: ['png'] }] // 文件过滤器 + }); + + if (canceled) { + console.log('用户取消了保存操作'); + return; + } + + // 将Buffer写入PNG文件到用户选择的路径 + fs.writeFile(filePath, buffer, (err) => { + if (err) { + console.error('写入文件失败:', err); + } else { + layer.msg(language_str("saveSucc") + filePath);//'保存成功至' + console.log('PNG文件已保存至:', filePath); + } + }); + } + function display_func(img1, img2, img3) { + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = []; + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + let list1 = [..._objs1] + for (let item of list1) { + let idx = getIndex(item, canvas1); + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = []; + left = 0; + top = 0; + black_top = 0; + black_left = 0; + all_top = 0; + all_left = 0; + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//黑色字或计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && (objs2[idx - 1].type == 10)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let fs = require('fs'); + const Buffer = require('buffer').Buffer; + const buffer = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + const printPath = path.join(exePath, 'print.png'); + fs.writeFileSync(printPath, buffer); + + layer.open({ + type: 1, + area: [w, h], + title: language_str("display"), //不显示标题栏"预览" + shadeClose: true, //点击遮罩关闭 + content: ` +
+
+
+ +
+ +
+
+
+
`, + btn: [language_str("output"), "打印"],//'导出' + btn1: function (index, layero) { + saveImageAsPNG(buffer) + + printJS({ printable: printPath, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } + }); + } + function output(callback = null, _save = save) { + // 类型改变 + fabric.Image.fromURL('./public/images/op_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + fabric.Image.fromURL('./public/images/op_1.png', function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = [];//黑色图层 + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + item.get("fill") == "#000000" + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + item.get("fill") == "#000000" + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = [];//黑色图层 + left = 0; + top = 0; + black_left = 0;//黑色左裁剪 + black_top = 0;//黑色顶裁剪 + all_left = 0;//黑色左裁剪 + all_top = 0;//黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + // let white_url2 = canvas2.getObjects()[0].toDataURL({ + // height: 2787, + // width: 2787, + // }); + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; + if (objs1.length != 0 & objs2.length == 0) { + op.flag = 2; + } else if (objs1.length == 0 & objs2.length != 0) { + op.flag = 3; + } + op.frontColorPic = desEncrypt(url1); + op.backColorPic = ''; + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = ''; + op.backBlackPic = ''; + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + if (idx == 0) continue;//background-image + let type = objs1[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 1, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + + } else if (type == 11) { + + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 3, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "Exceed": objs1[idx - 1].exceed, + "AutoWrap": objs1[idx - 1].autoWrap, + "FixSize": objs1[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs1[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 4, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].color, + "DefaultText": objs1[idx - 1].val, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 5, + "name": objs1[idx - 1].name, + "FontSize": parseInt(objs1[idx - 1].fontSize), + "DefaultText": objs1[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontFamily": objs1[idx - 1].font, + "FontColor": objs1[idx - 1].color, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 6, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": objs1[idx - 1].start, + "add": parseInt(objs1[idx - 1].add), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (idx == 0) continue;//background-image + let type = objs2[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 1, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + + } else if (type == 11) { + + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 3, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontSize": parseInt(item.get("fontSize")), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontColor": item.get("fill"), + "Exceed": objs2[idx - 1].exceed, + "AutoWrap": objs2[idx - 1].autoWrap, + "FixSize": objs2[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs2[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 4, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].color, + "DefaultText": objs2[idx - 1].val, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 5, + "name": objs2[idx - 1].name, + "DefaultText": objs2[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontSize": parseInt(objs2[idx - 1].fontSize), + "FontFamily": objs2[idx - 1].font, + "FontColor": objs2[idx - 1].color, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 6, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": parseInt(objs2[idx - 1].start), + "add": parseInt(objs2[idx - 1].add), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + _save(op, callback); + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + } + $('#output').on('click', function () { + output(null, saveAs); + return; + // 类型改变 + fabric.Image.fromURL('./public/images/op_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + fabric.Image.fromURL('./public/images/op_1.png', function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = [];//黑色图层 + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = [];//黑色图层 + left = 0; + top = 0; + black_left = 0;//黑色左裁剪 + black_top = 0;//黑色顶裁剪 + all_left = 0;//黑色左裁剪 + all_top = 0;//黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + // let white_url2 = canvas2.getObjects()[0].toDataURL({ + // height: 2787, + // width: 2787, + // }); + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; + if (objs1.length != 0 & objs2.length == 0) { + op.flag = 2; + } else if (objs1.length == 0 & objs2.length != 0) { + op.flag = 3; + } + op.frontColorPic = desEncrypt(url1); + op.backColorPic = ''; + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = ''; + op.backBlackPic = ''; + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + if (idx == 0) continue;//background-image + let type = objs1[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 1, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 3, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "Exceed": objs1[idx - 1].exceed, + "AutoWrap": objs1[idx - 1].autoWrap, + "FixSize": objs1[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs1[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 4, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].color, + "DefaultText": objs1[idx - 1].val, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 5, + "name": objs1[idx - 1].name, + "FontSize": parseInt(objs1[idx - 1].fontSize), + "DefaultText": objs1[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontFamily": objs1[idx - 1].font, + "FontColor": objs1[idx - 1].color, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 6, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": objs1[idx - 1].start, + "add": parseInt(objs1[idx - 1].add), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (idx == 0) continue;//background-image + let type = objs2[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 1, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 3, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontSize": parseInt(item.get("fontSize")), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontColor": item.get("fill"), + "Exceed": objs2[idx - 1].exceed, + "AutoWrap": objs2[idx - 1].autoWrap, + "FixSize": objs2[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs2[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 4, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].color, + "DefaultText": objs2[idx - 1].val, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 5, + "name": objs2[idx - 1].name, + "DefaultText": objs2[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontSize": parseInt(objs2[idx - 1].fontSize), + "FontFamily": objs2[idx - 1].font, + "FontColor": objs2[idx - 1].color, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 6, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": parseInt(objs2[idx - 1].start), + "add": parseInt(objs2[idx - 1].add), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'CS File Type', extensions: ['cs'] }, + ] + }).then(result => { + if (result.canceled) return; + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.cs'; + } + let fs = require('fs') + op.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(op)) + }).catch(err => { + console.log() + }) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + }); + var _keyctrl = false, _keyc = false, _textEdit = false, _inputEdit = false; + $("input").focus(function () { + console.log(1) + _inputEdit = true + }) + $("input").blur(function () { + _inputEdit = false + }) + $("textarea").focus(function () { + console.log(1) + _inputEdit = true + }) + $("textarea").blur(function () { + _inputEdit = false + }) + $("body").keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false; + }; + if (event.keyCode == 67) { + _keyc = false; + }; + }) + $("body").keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $("#new").click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $("#open").click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $("#save").click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + output(null, saveAs); + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $("#previous").click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $("#next").click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + pasteFromClipboard(); + return; + } + + // CTRL/CMD + SHIFT + DEL (Windows) 或 CMD + SHIFT + Backspace (Mac) - 清空 + if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#clean").click(); + return; + } + + // CTRL/CMD + DEL (Windows) 或 CMD + Backspace (Mac) - 删除 + if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#delete").click(); + return; + } + + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true; + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 67) { + _keyc = true; + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 37) { + //left + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + let x = canvas.getActiveObject().get("left"); + x += v; + canvas.getActiveObject().set("left", x); + canvas.renderAll(); + recordState(); + updateControls(); + } + if (event.keyCode == 38) { + //up + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + let y = canvas.getActiveObject().get("top"); + y += v; + canvas.getActiveObject().set("top", y); + canvas.renderAll(); + recordState(); + updateControls(); + } + if (event.keyCode == 39) { + //right + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + let x = canvas.getActiveObject().get("left"); + x += v; + canvas.getActiveObject().set("left", x); + canvas.renderAll(); + recordState(); + updateControls(); + } + if (event.keyCode == 40) { + //down + let v = +1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + let y = canvas.getActiveObject().get("top"); + y += v; + canvas.getActiveObject().set("top", y); + canvas.renderAll(); + recordState(); + updateControls(); + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + let actObjs = canvas.getActiveObjects(); + let allObjs = canvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1); + canvas.remove(item); + flag = 1; + + } + } + updateList(); + canvas.discardActiveObject(); + if (flag == 1) { + recordState(); + canvas.renderAll(); + handleObjectSelected() + } + } + }); + let pre_add_image; + $("#addPic").click(function () { + OpenDialog(); + function OpenDialog() { + dialog.showOpenDialog({ + title: language_str("selectPic"),//请选择图片 + buttonLabel: language_str("comf"),//"确认" + filters: [ + { name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }, + ] + }).then(result => { + if (result.canceled) return; + //console.log(result.filePaths) + fabric.Image.fromURL(result.filePaths[0], function (image) { + //image.left = pointer.x; + //image.top = pointer.y; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image; + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 1; + }); + //canvas.add(image); + //objs.push({type: 1}); + //canvas.renderAll(); + //updateList(); + //recordState(); + }); + }).catch(err => { + console.log(err) + }) + } + }); + $("#addOutPic").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 2; + }); + $("#addText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 3; + }); + + $("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; + }); + $("#addOutText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 4; + }); + $("#addRect").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 5; + }); + $("#addCircle").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 6; + }); + $("#addLine").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 7; + }); + $("#addQrCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 8; + }); + $("#addBarCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 9; + }); + $("#addCounter").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + addState = 10; + }); + $("#version_change").click(function () { + if (bg_version == 1) { + bg_version = 2; + } else { + bg_version = 1; + } + // 类型改变 + $("#source_front").attr("src", "./public/images/front_bg" + bg_version + "_1.png"); + canvas.renderAll(); + + }); + $("#front_side").click(function () { + $(this).addClass("ui-button-active"); + $("#back_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").show(); + $(".canvas-container:eq(1)").hide(); + console.log("------"); + console.log(recordObjs1); + console.log("------"); + canvas = canvas1; + objs = objs1; + background_image = background_image1; + next_objs = next_objs1; + pre_objs = pre_objs1; + next_json = next_json1; + pre_json = pre_json1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + updateList(); + + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//背景 + }); + $("#back_side").click(function () { + $(this).addClass("ui-button-active"); + $("#front_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").hide(); + $(".canvas-container:eq(1)").show(); + canvas = canvas2; + objs = objs2; + background_image = background_image2; + next_objs = next_objs2; + pre_objs = pre_objs2; + next_json = next_json2; + pre_json = pre_json2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + updateList(); + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + }); + $("#top_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set("top", background_image.get("top")); + canvas.renderAll(); + return; + } + let top = 0; + for (let item of _objs) { + let t_top = item.get("top"); + if (t_top < top) { + top = t_top; + } + } + groupItems = []; + for (let item of _objs) { + item.set("top", top); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + + }); + $("#bottom_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + _objs[0].set("top", background_image.get("top") + background_image.get("height") - _objs[0].get("height") * _objs[0].get("scaleY") - other); + canvas.renderAll(); + return; + } + let bottom = 0; + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + let t_b = item.get("top") + item.get("height") * item.get("scaleY") + other; + if (t_b > bottom) { + bottom = t_b; + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("top", bottom - item.get("height") * item.get("scaleY") - other); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + + }); + $("#center_y_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + _objs[0].set("top", background_image.get("top") + (background_image.get("height") - (_objs[0].get("height") + other) * _objs[0].get("scaleY")) / 2); + canvas.renderAll(); + return; + } + let top = 0; + for (let item of _objs) { + let t_top = item.get("top"); + if (t_top < top) { + top = t_top; + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("top", top + (item.group.height - (item.get("height") + other) * item.get("scaleY")) / 2); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#left_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set("left", background_image.get("left")); + canvas.renderAll(); + return; + } + let left = 0; + for (let item of _objs) { + let t_left = item.get("left"); + if (t_left < left) { + left = t_left; + } + } + groupItems = [] + for (let item of _objs) { + item.set("left", left); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#right_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + _objs[0].set("left", background_image.get("left") + background_image.get("width") - _objs[0].get("width") * _objs[0].get("scaleX") - other); + canvas.renderAll(); + return; + } + let right = 0; + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + let t_r = item.get("left") + item.get("width") * item.get("scaleX") + other; + if (t_r > right) { + right = t_r; + } + } + let groupItems = [] + + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("left", right - item.get("width") * item.get("scaleX") - other); + groupItems.push(item) + } + //BUG1 + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#center_x_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + _objs[0].set("left", background_image.get("left") + (background_image.get("width") - (_objs[0].get("width") + other) * _objs[0].get("scaleX")) / 2); + canvas.renderAll(); + return; + } + let left = 0; + for (let item of _objs) { + let t_left = item.get("left"); + if (t_left < left) { + left = t_left; + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("left", left + (item.group.width - (item.get("width") + other) * item.get("scaleX")) / 2); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#up_obj").click(function () {//上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + //console.log(objs); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + recordState(); + updateList(); + selectObj(idx); + } + }); + $("#down_obj").click(function () {//下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + recordState(); + updateList(); + selectObj(idx - 2); + } + }); + $("#set_bg").click(function () {//设置为背景 + let obj = canvas.getActiveObject(); + console.log(obj, obj.get("selectable")) + let idx = getIndex(obj); + const scaleX = 2787 / obj.width + const scaleY = 2787 / obj.height + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - 2787) / 2, + top: ($('#canvas1').height() - 2787) / 2, + }); + $('#text_w').val(2787) + $('#text_h').val(2787) + $('#text_x').val(0) + $('#text_y').val(0) + if (obj.get("selectable")) { + obj.set("selectable", false); + $("#lock_img").attr("src", "public/images/lock20.png") + } + canvas.sendToBack(obj); + canvas.bringForward(obj); + canvas.renderAll(); + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + recordState(); + updateList(); + selectObj(0); + }); + + // ========================================== + // 【新增】状态恢复后的回调函数 + // 作用:确保图片加载完毕后,再刷新界面,防止白屏或对象丢失 + // ========================================== + function onStateRestored() { + // 1. 重新获取背景图引用 (因为旧对象被销毁了) + // 注意:canvas 是全局变量,指向当前激活的画板 + if (canvas.getObjects().length > 0) { + background_image = canvas.getObjects()[0]; + background_image.selectable = false; // 确保背景不可选 + background_image.evented = false; + } + + // 2. 渲染画布 + canvas.requestRenderAll(); + + // 3. 取消选中状态 + selectObj(-1); + + // 4. 隐藏所有属性面板 + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + + // 5. 刷新左侧列表 + updateList(); + } + // ========================================== + // 撤销操作 (修复版) + // ========================================== + $("#previous").click(function () { + if (step.val == 0) return; + + step.val--; + + // 判断当前是正面还是背面 + if ($("#front_side").hasClass("ui-button-active")) { + // 1. 恢复业务数据 + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + // 2. 恢复画布 (使用回调函数!) + canvas.loadFromJSON(recordJson1[step.val], function() { + onStateRestored(); // 图片加载完才执行这一步 + }); + } else { + // 背面逻辑 + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + canvas.loadFromJSON(recordJson2[step.val], function() { + onStateRestored(); + }); + } + }); + // ========================================== + // 重做操作 (修复版) + // ========================================== + $("#next").click(function () { + // 获取当前历史记录的最大长度 + let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; + + if (step.val >= maxLen - 1) return; + + step.val++; + + if ($("#front_side").hasClass("ui-button-active")) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + canvas.loadFromJSON(recordJson1[step.val], function() { + onStateRestored(); // 图片加载完才执行这一步 + }); + } else { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + canvas.loadFromJSON(recordJson2[step.val], function() { + onStateRestored(); + }); + } + }); + + $("#clean").click(function () { + objs.splice(0, objs.length); + + let allObjs = canvas.getObjects(); + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item); + } + } + updateList(); + recordState(); + }); + $("#rotate").click(function () { + let obj = canvas.getActiveObject(); + obj.rotate(Math.round((obj.get("angle") + 90) % 360)); + $("#text_r").val(obj.get("angle")); + canvas.renderAll(); + updateControls(); + recordState(); + }); + $('#vertical_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipY", !obj.get("flipY")); + canvas.renderAll(); + updateControls(); + recordState(); + }); + $('#align_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipX", !obj.get("flipX")); + canvas.renderAll(); + updateControls(); + recordState(); + }); + $("#text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + + $("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + + $('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + recordState() + canvas.renderAll() + }) + $('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + recordState() + canvas.renderAll() + }) + $('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + recordState() + canvas.renderAll() + }) + + $("#out_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#out_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#out_text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#out_text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#lock").click(function () { + canvas.getActiveObject().set("selectable", !canvas.getActiveObject().get("selectable")); + if (canvas.getActiveObject().get("selectable")) { + $("#lock_img").attr("src", "public/images/unlock20.png")//已经上锁,需要解锁 + } else { + $("#lock_img").attr("src", "public/images/lock20.png")//没上锁,可以上锁 + } + recordState(); + canvas.renderAll(); + }); + $("#text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#text_back_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#circle_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#circle_text_back_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#out_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#out_text_back_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#rect_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#rect_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#rect_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#rect_stroke").val("#ffffff"); + canvas.renderAll(); + }); + $("#circle_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#circle_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#circle_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#circle_stroke").val("#ffffff"); + canvas.renderAll(); + }); + + function saveAs(op1, callback) { + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + let con_o = Object.assign(o, op1); + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + con_o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + if (callback && typeof callback === 'function') { + callback(); + } + }).catch(err => { + console.log() + }) + } + function save(op1, callback) { + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + let con_o = Object.assign(o, op1); + if (openAs.name != "") { + //打开的文件 + let fs = require('fs') + con_o.soonType = 1 + fs.writeFileSync(openAs.name, JSON.stringify(con_o)) + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + if (callback && typeof callback === 'function') { + callback(); + } + return; + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + con_o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + if (callback && typeof callback === 'function') { + callback(); + } + }).catch(err => { + console.log() + }) + } + + // =========================================================== + // 1. 剪贴板数据定义 (修改:改为存储数据 data,而不是对象引用) + // =========================================================== + let clipboardData = { + data: null, // 存储序列化后的 JSON 数据 + 业务属性 + offset: 10 // 粘贴位置偏移量 + }; + + // =========================================================== + // 2. 复制函数:深度拷贝当前选中对象的数据 + // =========================================================== + function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + layer.msg(language_str("selectPic") || "请先选择对象"); + return; + } + + // 【核心修复】 + // 遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 + clipboardData.data = activeObjects.map(obj => { + let idx = getIndex(obj); // 获取对象在画布中的索引 + + // 获取业务属性 (objs数组中对应的那一项) + // 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1 + let appData = (idx > 0 && objs[idx-1]) ? objs[idx-1] : { type: 0 }; + + return { + // 1. 保存画面样式 (坐标、颜色、字号等) + fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data']), + // 2. 保存业务属性 (深拷贝,防止引用冲突) + appData: JSON.parse(JSON.stringify(appData)) + }; + }); + + clipboardData.offset = 10; // 重置偏移量 + console.log(`已复制 ${activeObjects.length} 个对象`); + layer.msg("已复制"); + } + + // =========================================================== + // 3. 粘贴函数:根据保存的数据重建对象 + // =========================================================== + function pasteFromClipboard() { + // 检查是否有数据 + if (!clipboardData.data || clipboardData.data.length === 0) { + // layer.msg("剪贴板为空"); + return; + } + + // 1. 提取 Fabric JSON 准备恢复画面 + const jsonToEnliven = clipboardData.data.map(item => item.fabricJson); + + // 2. 恢复对象 (异步过程) + fabric.util.enlivenObjects(jsonToEnliven, function(objects) { + canvas.discardActiveObject(); + + objects.forEach((clone, index) => { + // --- A. 处理画面位置 --- + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + + // 如果是组内对象,需要特殊处理坐标 + if (clone.group) { + clone.set({ + left: clone.group.left + clipboardData.offset, + top: clone.group.top + clipboardData.offset + }); + } + + // --- B. 处理业务数据 --- + // 取出复制时保存的 appData + let savedAppData = clipboardData.data[index].appData; + let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝 + + // 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy) + if (newAppData.name) { + newAppData.name = newAppData.name + "_copy"; + } + + // --- C. 添加到画布和数据数组 --- + canvas.add(clone); + objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组 + }); + + // 3. 选中新粘贴出来的对象 + let sel = new fabric.ActiveSelection(objects, { canvas: canvas }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + + // 4. 刷新列表和历史记录 + updateList(); + recordState(); + + // 5. 高亮左侧列表最后一项 (即新粘贴的项) + if(objs.length > 0) selectObj(objs.length - 1); + + // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) + clipboardData.offset += 10; + }); + } + + // =========================================================== + // 4. 按钮事件绑定 (保持原样) + // =========================================================== + // 复制 + $("#copy").click(function () { + copySelection(); + }) + // 粘贴 + $("#paste").click(function () { + pasteFromClipboard(); + }) + + // 保存 (保持你原有的保存逻辑不变) + $("#save").click(function () { + output(); // 这里调用你的 output 函数 + return; + // ... (下面是你原有的被 return 截断的代码,建议以后清理掉,但这里我按要求保留) + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + // ... (省略你原来那段很长的保存代码) ... + }); + + + function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); + } + function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")] + } + let fullPath = path.join(exePath, 'data.json') + // ========================================== + // 历史记录保存 (修复结构Bug + 自动置顶) + // ========================================== + function saveHistory() { + let fs = require('fs'); + let j = { history: [] }; // 默认结构必须是数组 + + try { + // 1. 读取并解析现有文件 + if (fs.existsSync(fullPath)) { + let content = fs.readFileSync(fullPath).toString(); + // 防止空文件报错 + if (content.trim()) { + j = JSON.parse(content); + } + } + } catch (e) { + console.error("读取历史记录失败,将重置:", e); + // 出错时使用默认空数组,不中断流程 + j = { history: [] }; + } + + // 确保 j.history 是数组 (防御性编程) + if (!Array.isArray(j.history)) { + j.history = []; + } + + // 2. 【核心优化】先过滤掉当前文件(如果已存在),然后加到最前面 + // 这样可以实现“最近使用的文件排在第一位”的效果 + let currentPath = openAs.name; + let newList = j.history.filter(item => item.path !== currentPath); + + // 3. 插入到头部 (Unshift) + newList.unshift({ + time: getDate(), + path: currentPath, + // type: 1 // 如果需要记录文件类型也可以加在这里 + }); + + // 4. (可选) 限制历史记录数量,比如只保留最近20条,防止文件无限膨胀 + if (newList.length > 20) { + newList = newList.slice(0, 20); + } + + j.history = newList; + + // 5. 统一写入 + try { + fs.writeFileSync(fullPath, JSON.stringify(j)); + } catch (e) { + console.error("写入历史记录失败:", e); + } + } + + function open(file) { + console.log(file) + let fs = require('fs') + var fsData = fs.readFileSync(file) + openAs.name = file; + let j = JSON.parse(fsData.toString()); + let left = background_image.left; + let top = background_image.top; + let new_left = j.f.objects[0].left; + let new_top = j.f.objects[0].top; + for (let o of j.f.objects) { + o.left = o.left - new_left + left; + o.top = o.top - new_top + top; + } + // for (let o of j.b.objects) { + // o.left = o.left - new_left + left; + // o.top = o.top - new_top + top; + // } + console.log(j.f) + canvas1.loadFromJSON(j.f); + canvas2.loadFromJSON(j.b); + background_image1 = canvas1.getObjects()[0]; + background_image2 = canvas2.getObjects()[0]; + + objs2 = j.bo; + objs1 = j.fo; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + background_image = background_image1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + background_image = background_image2; + step = step2; + } + canvas1.renderAll(); + canvas2.renderAll(); + updateList(); //修复打开新文件后无法撤销的bug + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j1.objects[0].hoverCursor = "default"; + recordJson1.push(j1); + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + // j2.objects[0].hoverCursor = "default"; + recordJson2.push(j2); + }, 0); + } + $("#open").click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件? + output(OpenDialog); + layer.close(index); + + }, function (index) { + OpenDialog(); + }); + function OpenDialog() { + dialog.showOpenDialog({ + title: "请选择文件",//confirm + buttonLabel: language_str("comf"),//"确认" + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.canceled) return; + console.log(result.filePaths) + let fs = require('fs') + var fsData = fs.readFileSync(result.filePaths[0]) + openAs.name = result.filePaths[0]; + let j = JSON.parse(fsData.toString()); + let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + if (type == 2) { + ipcRenderer.send('open-design-page', openAs.name, type); + return + } + let left = background_image.left; + let top = background_image.top; + let new_left = j.f.objects[0].left; + let new_top = j.f.objects[0].top; + for (let o of j.f.objects) { + o.left = o.left - new_left + left; + o.top = o.top - new_top + top; + } + // for (let o of j.b.objects) { + // o.left = o.left - new_left + left; + // o.top = o.top - new_top + top; + // } + canvas1.loadFromJSON(j.f); + canvas2.loadFromJSON(j.b); + background_image1 = canvas1.getObjects()[0]; + background_image2 = canvas2.getObjects()[0]; + + objs2 = j.bo; + objs1 = j.fo; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + background_image = background_image1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + background_image = background_image2; + step = step2; + } + canvas1.renderAll(); + canvas2.renderAll(); + updateList(); + // 修复打开新文件后无法撤销的bug + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j1.objects[0].hoverCursor = "default"; + recordJson1.push(j1); + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j2.objects[0].hoverCursor = "default"; + recordJson2.push(j2); + }, 0); + }).catch(err => { + console.log(err) + }) + } + }); + function clearAll() { + //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; + //let j = JSON.parse(blank); + //canvas1.setZoom(1); + //canvas2.setZoom(1); + //canvas1.loadFromJSON(j.f); + //canvas2.loadFromJSON(j.b); + let t_objs1 = canvas1.getObjects(); + let t_objs2 = canvas2.getObjects(); + for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]); + } + for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]); + } + objs2 = []; + objs1 = []; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + } + openAs.name = ""; + canvas1.renderAll(); + canvas2.renderAll(); + updateList(); + console.log(); + } + $("#new").click(function () { + if (step.val == 0) { + ipcRenderer.send('open-first-page'); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件? + output(() => ipcRenderer.send('open-first-page')); + return; + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + if (openAs.name != "") { + //打开的文件 + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page'); + //saveHistory(); + //clearAll(); + return; + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page'); + //saveHistory(); + //clearAll(); + }).catch(err => { + console.log() + }) + layer.close(index); + + }, function (index) { + /* + 2022-07-16 直接跳转至首页 + */ + //clearAll(); + ipcRenderer.send('open-first-page'); + }); + }); + function chagneTitle() { + + } + function addBackground() { + // 类型改变 + fabric.Image.fromURL('./public/images/bg_front_1.png', function (image) { + let width = $("#canvas1").width() + + image.left = ($("#canvas1").width() - image.width * image.get("scaleX")) / 2; + image.top = ($("#canvas1").height() - image.height * image.get("scaleY")) / 2; + + image.selectable = false; + image.hoverable = false; + image.hoverCursor = "default" + image.setSrc(image.toDataURL(), function (image) { + image.scale(1, 1); + background_image1 = image; + background_image = background_image1; + canvas.add(image); + canvas.renderAll(); + //pre_objs1.push(JSON.stringify(objs1)); + //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs1.push(JSON.stringify(objs1)); + recordJson1.push(canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"])); + }); + // 类型改变 + fabric.Image.fromURL('./public/images/bg_back.png', function (image) { + image.left = ($("#canvas2").width() - image.width) / 2; + image.top = ($("#canvas2").height() - image.height) / 2; + + image.selectable = false; + image.hoverable = false; + image.hoverCursor = "default" + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image; + canvas2.add(image); + canvas2.renderAll(); + //pre_objs2.push(JSON.stringify(objs2)); + //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs2.push(JSON.stringify(objs2)); + recordJson2.push(canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"])); + + let file = GetFile().get('file') + console.log(file) + if (file && file != 'empty') { + open(file) + } + }); + }); + is_bgi_add = true; + }); + } + function selectObject(target, index) { + updateControls(); + + $("#base_control").show(); + //$("#text_x").val((target.left - background_image.left).toFixed(1)); + //$("#text_y").val((target.top - background_image.top).toFixed(1)); + //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); + //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); + //$("#text_r").val(target.get("angle").toFixed(0)); + } + function updateControls() { + $("#text_r").val(canvas.getActiveObject().get("angle").toFixed(0)); + $("#text_x").val((canvas.getActiveObject().get("left") - background_image.left).toFixed(0)); + $("#text_y").val((canvas.getActiveObject().get("top") - background_image.top).toFixed(0)); + $("#text_h").val((canvas.getActiveObject().get("scaleY") * canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("scaleX") * canvas.getActiveObject().get("width")).toFixed(0)); + if (canvas.getActiveObject().get("selectable")) { + $("#lock_img").attr("src", "public/images/unlock20.png")//已经上锁,需要解锁 + } else { + $("#lock_img").attr("src", "public/images/lock20.png")//没上锁,可以上锁 + } + } + function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; + } + function resName(pre_name) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num));//重复了就再加1 + return pre_name + num; + } + function addPic(pointer) { + + pre_add_image.left = pointer.x; + pre_add_image.top = pointer.y; + + canvas.add(pre_add_image); + + objs.push({ type: 1, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + //OpenDialog(); + //function OpenDialog() + //{ + // dialog.showOpenDialog({ + // title: "请选择图片", + // buttonLabel: "确认", + // filters: [ + // { name: 'Image File Type', extensions: ['png', 'jpg'] }, + // ] + // }).then(result => { + // if(result.canceled)return; + // //console.log(result.filePaths) + // fabric.Image.fromURL(result.filePaths[0], function(image) { + // image.left = pointer.x; + // image.top = pointer.y; + // + // canvas.add(image); + // objs.push({type: 1}); + // canvas.renderAll(); + // updateList(); + // recordState(); + // } ); + // }).catch(err => { + // console.log(err) + // }) + //} + } + function selectPic(target, index) { + //透明度 + + $("#pic_control").show(); + $("#set_bg").show(); + pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + + let idx = getIndex(canvas.getActiveObject()); + // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function addOutPic(pointer) { + fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + let width = canvas.getWidth(); + let height = canvas.getHeight(); + image.left = pointer.x; + image.top = pointer.y; + image.opacity = 1; + //image.on("selected",function(){//test well + // console.log(222); + //}); + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image); + objs.push({ type: 2, name: resName("OUT_PIC_"), black: false }); + canvas.renderAll(); + updateList(); + recordState(); + }); + }); + } + function selectOutPic(target, index) { + //字段名 + $("#out_pic_control").show(); + out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + let idx = getIndex(canvas.getActiveObject()); + $("#out_pic_field_name").val(objs[idx - 1].name); + // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + + function addText(pointer) { + let text = new fabric.Text('TEXT', {//源:文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + fontSize: 100, + underline: false, + linethrough: false, + fontFamily: 'Arial' + }); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 3, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectText() { + //console.log(canvas.getActiveObject().get("fontFamily")); + $("#text_control").show(); + $("#text_color").val(canvas.getActiveObject().get("fill")); + $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#text_size").val(val); + $("#text_text").val(canvas.getActiveObject().get("text")); + $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#text_underline").addClass("ui-radio-acitve") + } else { + $("#text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#text_bold").addClass("ui-radio-acitve") + } else { + $("#text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#text_italics").addClass("ui-radio-acitve") + } else { + $("#text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#text_linethrough").addClass("ui-radio-acitve") + } else { + $("#text_linethrough").removeClass("ui-radio-acitve") + } + $("#circle_text_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; + } + function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180/Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg + } + function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width/2,canvas.height/2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance*2, + fontSize: 100, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + // originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height/2 ? true : false + }); + // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) + // text.set({diameter: distance*2}) + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectCircleText() { + let idx = getIndex(canvas.getActiveObject()); + $("#circle_text_control").show(); + $("#circle_text_color").val(canvas.getActiveObject().get("fill")); + $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); + circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")-500); + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(canvas.getActiveObject().get("text")); + $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function addOutText(pointer) { + //MERGE_TEXT_1 + let text = new fabric.Text("MERGE TEXT", {//源:合成文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + fontStyle: "normal", + fontWeight: "normal", + fontSize: 100, + textBackgroundColor: "", + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }); + + //text.on({ + // 'scaling': function(e) { + // var obj = this, + // w = obj.width * obj.scaleX, + // h = obj.height * obj.scaleY; + // + // obj.set({ + // 'height' : h, + // 'width' : w, + // 'scaleX' : 1, + // 'scaleY' : 1 + // }); + // } + //}); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + + canvas.add(text); + objs.push({ type: 4, name: resName("OUT_TEXT_"), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectOutText() { + //color + //size + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $("#out_text_control").show(); + $("#out_text_color").val(canvas.getActiveObject().get("fill")); + $("#out_text_text").val(canvas.getActiveObject().get("text")); + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#out_text_size").val(val); + $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let idx = getIndex(canvas.getActiveObject()); + $("#out_text_field_name").val(objs[idx - 1].name); + $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); + $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); + $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); + if (canvas.getActiveObject().get("underline") == true) { + $("#out_text_underline").addClass("ui-radio-acitve") + } else { + $("#out_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#out_text_bold").addClass("ui-radio-acitve") + } else { + $("#out_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#out_text_italics").addClass("ui-radio-acitve") + } else { + $("#out_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#out_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#out_text_linethrough").removeClass("ui-radio-acitve") + } + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 10, + width: 300, + height: 300 + }); + canvas.add(rect); + objs.push({ type: 5, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectRect() { + //color + $("#rect_control").show(); + $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + $("#rect_radius").val(canvas.getActiveObject().get("rx")); + rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + let a = canvas.getActiveObject().getCoords(); + console.log(getCoordsMinX(a)); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 10, + radius: 150, + strokeUniform: true, + }); + canvas.add(circle); + objs.push({ type: 6, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectCircle() { + //border-color + //color + $("#circle_control").show(); + $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addLine(pointer) { + let line = new fabric.Line([100, 200, 400, 200], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 10, + strokeDashArray: [0, 0], + }); + + line.setControlVisible("tl", false); + line.setControlVisible("tr", false); + line.setControlVisible("br", false); + line.setControlVisible("bl", false); + line.setControlVisible("mt", false); + line.setControlVisible("mb", false); + canvas.add(line); + objs.push({ type: 7, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectLine() { + //color + //weight + $("#line_control").show(); + + let idx = getIndex(canvas.getActiveObject()); + $("#line_color").val(canvas.getActiveObject().get("stroke")); + $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addQrCode(pointer) { + + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }); + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x; + image.top = pointer.y; + image.scale(2.4, 2.4) + canvas.add(image); + objs.push({ type: 8, name: resName("QR_CODE_"), color: "#000000", val: "https://www.cardsoon.com", black: false }); + canvas.renderAll(); + updateList(); + recordState(); + }); + } + function selectQrCode() { + //字段名 + //background-color + //color + $("#qrcode_Controll").show(); + let idx = getIndex(canvas.getActiveObject()); + $("#qrcode_field_name").val(objs[idx - 1].name); + $("#qrcode_color").val(objs[idx - 1].color); + $("#qrcode_val").val(objs[idx - 1].val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#barcode_controll").hide(); + } + function addBarCode(pointer) { + JsBarcode("#barcode", "123456789", { margin: 0, lineColor: "#000000", fontSize: 18, font: "Arial" }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x; + image.top = pointer.y; + image.scale(2.4, 2.4) + canvas.add(image); + objs.push({ type: 9, name: resName("BAR_CODE_"), val: "123456789", color: "#000000", fontSize: 18, font: "Arial", show: false, black: false });//show是取消文字显示 + canvas.renderAll(); + updateList(); + recordState(); + }); + } + function selectBarCode() { + //字段名 + //background-color + //color + $("#barcode_controll").show(); + let idx = getIndex(canvas.getActiveObject()); + $("#barcode_field_name").val(objs[idx - 1].name); + $("#barcode_font_family").val(objs[idx - 1].font); + let val = parseFloat(objs[idx - 1].fontSize)*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#barcode_size").val(val); + $("#barcode_val").val(objs[idx - 1].val); + $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + } + function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fontSize: 100, + fill: "#000000", + }); + canvas.add(text); + objs.push({ type: 10, name: resName("COUNTER_"), start: 0, add: 1, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + console.log(canvas.getObjects()) + } + function selectCounter() { + //起始 + //终止 + //增量 + $("#counter_control").show(); + + + let idx = getIndex(canvas.getActiveObject()); + $("#counter_start").val(objs[idx - 1].start); + $("#counter_add").val(objs[idx - 1].add); + $("#counter_color").val(canvas.getActiveObject().get("fill")); + $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#count_size").val(val); + + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function getIndex(target, _canvas = null) { + let temp_objs; + if (_canvas == null) { + temp_objs = canvas.getObjects(); + } else { + temp_objs = _canvas.getObjects(); + } + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; + } + function getType(target) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } + } + // =========================================================== + // 1. 刷新对象列表 (序号顺序:底层=1,顶层=N,修复:Flex布局防遮挡 + 序号靠右) + // =========================================================== + function updateList() { + let str = ""; + let i = 0; + + // 定义行样式:Flex布局,两端对齐,相对定位防止溢出 + const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; + // 定义左侧内容样式 + const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; + // 定义序号样式:靠右,有左边框 + const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; + + for (let item of objs) { + let displayIndex = i + 1; // 序号底层为1 + + // 图标逻辑 + let iconName = "rect.svg"; + switch (item.type) { + case 1: iconName = "images.svg"; break; + case 2: iconName = "images_add.svg"; break; + case 3: iconName = "text.svg"; break; + case 4: iconName = "text2.svg"; break; + case 5: iconName = "rect.svg"; break; + case 6: iconName = "round.svg"; break; + case 7: iconName = "line.svg"; break; + case 8: iconName = "qrcode.svg"; break; + case 9: iconName = "barcode.svg"; break; + case 10: iconName = "counter.svg"; break; + case 11: iconName = "text3.svg"; break; + } + + // 名称逻辑 + let nameText = item.name ? item.name : + (item.type === 3 ? "TEXT_" + displayIndex : + (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); + + // 组装 HTML + str = `
+
+ + ${nameText} +
+ ${displayIndex} +
` + str; + i++; + } + $(".obj_list").html(str); + + // 重新绑定点击事件(防止 updateList 后点击失效) + $(".obj_list > div").on("click", function () { + let imgId = $(this).find("img").attr("id"); + if(imgId) { + let idx = parseInt(imgId.replace("obj", "")); + selectObj(idx); + } + }); + } + + // =========================================================== + // 2. 列表选中高亮 (修复:自动滚动 + 防冲突) + // =========================================================== + function selectObj(objIndices, isFromCanvas = false) { + let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); + + // 清除所有高亮 + $('.obj_list > div').css("background-color", "transparent"); + + // 遍历列表查找并高亮 + $('.obj_list > div').each(function () { + let $img = $(this).find("img"); + if ($img.length === 0) return; + + let idIndex = parseInt($img.attr("id").replace("obj", "")); + + if (indicesArray.includes(idIndex)) { + $(this).css("background-color", "#6F7A84"); + + // 自动滚动到可视区域 + if (isFromCanvas && indicesArray[0] === idIndex) { + this.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + } + }); + + // 如果是点击列表,触发画布选中 + if (!isFromCanvas && indicesArray.length === 1) { + let target = canvas.getObjects()[indicesArray[0] + 1]; + if (target) { + canvas.setActiveObject(target); + canvas.requestRenderAll(); + handleObjectSelected(); + } + } + } + function recordState() { + //pre_objs.push(JSON.stringify(objs)); + //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //next_json = []; + //next_objs = []; + //recordObjs.push(JSON.stringify(objs)); + //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + ++step.val; + for (let i = step.val; i < recordObjs.length; i++) { + recordObjs.pop(); + recordJson.pop(); + } + //recordObjs.splice(step.val, 1, JSON.stringify(objs)) + //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) + recordObjs.push(JSON.stringify(objs)) + j = canvas.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j.objects[0].hoverCursor = "default"; + recordJson.push(j); + } + function getCoordsMinX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x; + } + function getCoordsMaxX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x; + + } + function getCoordsMinY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y; + + } + function getCoordsMaxY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y; + } + + $("#text_y").bind('input propertychange', function () { + if ($("#text_y").val() == "") { + $("#text_y").val(0) + } + let val = background_image.top; + if ($("#text_y").val() != "") { + val = background_image.top + parseInt($("#text_y").val()); + } + canvas.getActiveObject().set("top", val) + canvas.renderAll(); + recordState(); + }) + $("#text_x").bind('input propertychange', function () { + if ($("#text_x").val() == "") { + $("#text_x").val(0) + } + let val = background_image.left; + if ($("#text_x").val() != "") { + val = background_image.left + parseInt($("#text_x").val()); + } + canvas.getActiveObject().set("left", val) + canvas.renderAll(); + recordState(); + }) + $("#text_w").bind('input propertychange', function () { + let val = "0"; + if ($("#text_w").val() != "") { + val = $("#text_w").val(); + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("width", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleX", parseInt(val) / canvas.getActiveObject().get("width")) + } + + canvas.renderAll(); + recordState(); + }) + $("#text_h").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + if (objs[idx - 1].type == 7) { + if ($("#text_h").val() == "") { + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")); + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#text_h").val())); + canvas.renderAll(); + return; + } + let val = "0"; + if ($("#text_h").val() != "") { + val = $("#text_h").val(); + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("height", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleY", parseInt(val) / canvas.getActiveObject().get("height")) + } + + canvas.renderAll(); + recordState(); + }) + $("#text_r").bind('input propertychange', function () { + + if ($("#text_r").val() == "") { + $("#text_r").val("0") + } + $("#text_r").val(parseInt($("#text_r").val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($("#text_r").val())) + canvas.renderAll(); + recordState(); + }) + $("#out_pic_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_pic_field_name").val(); + updateList(); + recordState(); + }) + + $("#text_black").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].black = true; + } else { + objs[idx - 1].black = false; + } + recordState(); + }); + $("#out_text_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_text_field_name").val(); + updateList(); + recordState(); + }) + $("#qrcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#qrcode_field_name").val(); + updateList(); + recordState(); + }) + $("#qrcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64($("#qrcode_val").val(), { padding: 0, foreground: $(this).val() }); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#qrcode_val").bind('change', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#barcode_field_name").val(); + updateList(); + recordState(); + }) + $("#text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#text_back_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll(); + updateList(); + recordState(); + }) + $("#out_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#out_text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#out_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#out_text_back_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + // 监听合成文本输入框 (占位符) + $("#out_text_text").on('input propertychange', function () { + let val = $(this).val(); + + // 【核心修改】如果值为空,强制给 2 个空格 + // 这样宽度足够宽,容易点击选中 + if (val === "") { + val = " "; + } + + let obj = canvas.getActiveObject(); + if (obj) { + obj.set("text", val); + obj.setCoords(); + canvas.requestRenderAll(); + updateList(); + recordState(); + } + }); + $("#rect_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#rect_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#rect_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#rect_stroke").val()); + + canvas.renderAll(); + recordState(); + }) + $("#rect_stroke_width").bind('input propertychange', function () { + if ($("#rect_stroke_width").val() == "") { + $("#rect_stroke_width").val("0") + } else { + $("#rect_stroke_width").val(parseInt($("#rect_stroke_width").val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#rect_stroke_width").val())); + canvas.renderAll(); + recordState(); + }) + $("#rect_radius").bind('input propertychange', function () { + if ($("#rect_radius").val() == "") { + $("#rect_radius").val("0") + } else { + $("#rect_radius").val(parseInt($("#rect_radius").val())) + } + canvas.getActiveObject().set("rx", parseInt($("#rect_radius").val())); + canvas.getActiveObject().set("ry", parseInt($("#rect_radius").val())); + canvas.renderAll(); + recordState(); + }) + $("#circle_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#circle_stroke").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_stroke_width").bind('input propertychange', function () { + if ($("#circle_stroke_width").val() == "") { + $("#circle_stroke_width").val("0") + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#circle_stroke_width").val())); + canvas.renderAll(); + recordState(); + }) + $("#counter_start").bind('input propertychange', function () { + canvas.getActiveObject().set("text", $("#counter_start").val()); + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].start = $(this).val(); + canvas.renderAll(); + recordState(); + }) + $("#counter_add").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].add = $(this).val(); + canvas.renderAll(); + recordState(); + }) + $("#counter_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#counter_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $("#line_color").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#line_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#line_dist").bind('input propertychange', function () { + if ($("#line_dist").val() == "") { + $("#line_dist").val(0) + } + canvas.getActiveObject().set("strokeDashArray", [parseInt($("#line_dist").val()), parseInt($("#line_dist").val())]) + canvas.renderAll(); + recordState(); + }) + // 监听普通文本输入 (占位符) + $("#text_text").on('input propertychange', function () { + // 1. 获取输入框的值,不要用 trim(),否则无法输入空格 + let val = $(this).val(); + + // 2. 核心修改:只有当删光时,才给 4 个空格作为占位符 + if (val === "") { + val = " "; // 4个空格,比原来的2个更宽,更好点 + } + // 3. 更新对象 + // 注意:这里直接用 val,不再用 ` ${val} `,这样有文字时就不会强制加空格了 + canvas.getActiveObject().set("text", val); + canvas.requestRenderAll(); // 使用 requestRenderAll 性能更好 + updateList(); + recordState(); + }); + $("#barcode_val").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].val = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }) + $("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val() + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + val = 0 + } else { + objs[idx - 1].show = false; + } + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val*dpi/72, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val*dpi/72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }) + $('#count_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + $('#text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + + $('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + + $('#language_select').on('change', function () {//语言选择 + langua_ge($(this).find('option:selected').val()); + s_lan = $(this).find('option:selected').val(); + localStorage.setItem("lang", s_lan); + }); + $('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].font = $(this).find('option:selected').text(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + //recordState(); + }); + $('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + $(".obj_list").on("click", "div", function () { + let idx = objs.length - 1 - $(this).index(); + selectObj(idx); + }); + $("#exceed").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].exceed = true; + } else { + objs[idx - 1].exceed = false; + } + recordState(); + }); + $("#autoWrap").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].autoWrap = true; + } else { + objs[idx - 1].autoWrap = false; + } + recordState(); + }); + $("#fixSize").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].fixSize = true; + } else { + objs[idx - 1].fixSize = false; + } + recordState(); + }); + $("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + recordState(); + }); + $("#delete").click(function () { + let actObjs = canvas.getActiveObjects(); + let allObjs = canvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1); + canvas.remove(item); + flag = 1; + + } + } + updateList(); + canvas.discardActiveObject(); + if (flag == 1) { + recordState(); + canvas.renderAll(); + } + // let actObjs = canvas.getActiveObjects(); + // let allObjs = canvas.getObjects(); + // let flag = 0; + // for( let item of actObjs){ + // if ( item != allObjs[0]){ + // let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + // objs.splice(i-1,1); + // canvas.remove(item); + // flag = 1; + // updateList(); + // } + // } + // selectObj(-1); + // if(flag == 1){ + // recordState(); + // } + }); + + //在关于我们页面,获取版本号。 + // 注意:function 前面加了 async 关键字 + $("#about").click(async function () { + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + console.error("获取版本号失败", e); + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
' + + '
' + + '
Soon Design
' + + // 【修改点】这里使用了刚才获取到的 version 变量 + '
' + language_str("vers") + ' ' + version + '
' + + '
' + language_str("copyright") + '
' + + '
' + }); + }) + $("#help").click(() => { + ipcRenderer.send('open-help-file'); + }); + + + // =========================================================== + // 对象选中处理 + // =========================================================== + function handleObjectSelected() { + let select_obj = canvas.getActiveObject(); + + // 1. 未选中任何对象 + if (!select_obj) { + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); // 传 true,只更新UI + return; + } + + // 2. 获取索引 + let i = getIndex(select_obj); + + if (i != 0) { + // --- 选中了普通对象 --- + --i; // 转换为 objs 数组下标 + + if(objs[i]) { + switch (objs[i].type) { + case 1: // 图片 + $("#component_type").text(language_str("img")); + selectObject(select_obj, i); selectPic(select_obj, i); break; + case 2: // 合成图片 + $("#component_type").text(language_str("simg")); + selectObject(select_obj, i); selectOutPic(select_obj, i); break; + case 3: // 文本 + $("#component_type").text(language_str("text")); + selectObject(select_obj, i); selectText(select_obj, i); break; + case 4: // 合成文本 + $("#component_type").text(language_str("stext")); + selectObject(select_obj, i); selectOutText(select_obj, i); break; + case 5: // 矩形 + $("#component_type").text(language_str("rect")); + selectObject(select_obj, i); selectRect(select_obj, i); break; + case 6: // 圆形 + $("#component_type").text(language_str("circ")); + selectObject(select_obj, i); selectCircle(select_obj, i); break; + case 7: // 直线 + $("#component_type").text(language_str("line")); + selectObject(select_obj, i); selectLine(select_obj, i); break; + case 8: // 二维码 + $("#component_type").text(language_str("qrc")); + selectObject(select_obj, i); selectQrCode(select_obj, i); break; + case 9: // 条形码 + $("#component_type").text(language_str("barc")); + selectObject(select_obj, i); selectBarCode(select_obj, i); break; + case 10: // 计数器 + $("#component_type").text(language_str("counter")); + selectObject(select_obj, i); selectCounter(select_obj, i); break; + case 11: // 圆形文本 + $("#component_type").text(language_str("ctext")); + selectObject(select_obj, i); selectCircleText(select_obj, i); break; + } + } + + // 【关键】这里传入 true! + // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 + selectObj(i, true); + + } else { + // --- 选中了背景 --- + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + selectObj(-1, true); + $("#component_type").text(language_str("bg")); + } + } + + // =========================================================== + // 统一事件监听 (替代原有的 canvas1.on 和 canvas2.on) + // =========================================================== + // 提取对象移动逻辑 (供正反面复用) + function handleObjectMoving(e) { + let obj = e.target; + let idx = getIndex(obj); + // 圆形文本特殊处理 + if (idx > 0 && idx <= objs.length && objs[idx - 1] && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { x: obj.left, y: obj.top }; + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); + let angleDeg = getDeg(pointer); + let flipped = pointer.y > canvas.height / 2 ? true : false; + + if(typeof circle_text_diameter !== 'undefined') circle_text_diameter.setValue(distance * 2 - 500); + obj.set("diameter", distance * 2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); + } + + // 启动监听 + // =========================================================== + // 统一事件绑定 (已集成边界检测) + // =========================================================== +// =========================================================== + // 3. 统一事件绑定 (修复:使用 Global Composite Operation 实现完美遮罩) + // =========================================================== + function initCanvasEvents() { + [canvas1, canvas2].forEach(currentCanvas => { + + // 1. 鼠标移动 + currentCanvas.on("mouse:move", (e) => { + let offsetX = 0, offsetY = 0; + if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { + offsetX = background_image.left; + offsetY = background_image.top; + } + if(e.pointer) { + $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); + } + updateRulerGuides(e.e); + }); + + // 2. 鼠标移出 + currentCanvas.on("mouse:out", () => { + $("#ruler-guide-h").hide(); $("#ruler-guide-v").hide(); + }); + + // 3. 选中处理 + currentCanvas.on('mouse:down', handleObjectSelected); + + // 4. 鼠标松开 (添加对象) + currentCanvas.on("mouse:up", (e) => { + if (addState === 0) return; + const ptr = e.pointer; + switch (addState) { + case 1: addPic(ptr); break; + case 2: addOutPic(ptr); break; + case 3: addText(ptr); break; + case 4: addOutText(ptr); break; + case 5: addRect(ptr); break; + case 6: addCircle(ptr); break; + case 7: addLine(ptr); break; + case 8: addQrCode(ptr); break; + case 9: addBarCode(ptr); break; + case 10: addCounter(ptr); break; + case 11: addCircleText(ptr); break; + } + addState = 0; + if(typeof background_image !== 'undefined') background_image.hoverCursor = "default"; + currentCanvas.renderAll(); + $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); + }); + + // 5. 对象移动 (只保留圆形文本逻辑,去掉了 checkOutOfBounds) + currentCanvas.on("object:moving", (e) => { + handleObjectMoving(e); + }); + + // 6. 变换事件 + currentCanvas.on("object:rotating", updateControls); + currentCanvas.on("object:scaling", updateControls); + currentCanvas.on("object:rotated", () => { recordState(); }); + currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); + currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); + currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); + + // 7. 对象修改后 + currentCanvas.on('object:modified', function (event) { + if (currentCanvas.getActiveObjects().length > 1) return; + let idx = getIndex(event.target); + if(idx <= 0) return; + let objData = objs[idx - 1]; + let target = event.target; + + if ([3, 4, 10].includes(objData.type)) { + target.fontSize = (target.fontSize * target.scaleX).toFixed(0); + target.scaleX = 1; target.scaleY = 1; + target._clearCache(); + if(objData.type == 3) $("#text_size").val(target.fontSize); + if(objData.type == 4) $("#out_text_size").val(target.fontSize); + if(objData.type == 10) $("#count_size").val(target.fontSize); + currentCanvas.renderAll(); + updateControls(); + } else if (objData.type == 5) { + target.width *= target.scaleX; target.height *= target.scaleY; + target.scaleX = 1; target.scaleY = 1; + updateControls(); + } + recordState(); + }); + // ================================================== + // 8. 【完美修复版】渲染后绘制:外部蒙版 + 中心圆孔 (修复三角形Bug) + // ================================================== + currentCanvas.on('after:render', function () { + let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2; + + if (bgObj) { + const ctx = currentCanvas.getContext(); + const width = currentCanvas.width; + const height = currentCanvas.height; + + // 1. 计算圆心 + const cx = bgObj.left + (bgObj.width * bgObj.scaleX) / 2; + const cy = bgObj.top + (bgObj.height * bgObj.scaleY) / 2; + + // 2. 计算半径 + // 外半径 (光盘大小) + const r_outer = (bgObj.width * bgObj.scaleX) / 2; + + // 内半径 (中心孔) + const dpi = 600; + const mmToPx = dpi / 25.4; + let holeDiameterMm = 24; // 默认 15mm + + // 切换版本判断 (15mm vs 38mm) + if (currentCanvas === canvas1 && typeof bg_version !== 'undefined' && bg_version === 2) { + holeDiameterMm = 42; + } + + // 比例计算法:确保孔径随缩放适配 + // r_inner = r_outer * (孔径 / 光盘标准直径120mm) + const r_inner = r_outer * (holeDiameterMm / 120); + + ctx.save(); + ctx.beginPath(); + + // A. 绘制全屏矩形 (顺时针) + ctx.rect(0, 0, width, height); + + // B. 挖掉外圆 (逆时针) + // 【关键修复】画圆前,必须把笔移动到圆的起点 (cx + r, cy) + // 否则 Canvas 会画一条线连过来,形成奇怪的三角形 + ctx.moveTo(cx + r_outer, cy); + ctx.arc(cx, cy, r_outer, 0, Math.PI * 2, true); + + // C. 填补内圆 (顺时针) + // 【关键修复】同理,把笔移动到内圆的起点 + ctx.moveTo(cx + r_inner, cy); + ctx.arc(cx, cy, r_inner, 0, Math.PI * 2, false); + + ctx.closePath(); + ctx.fillStyle = "rgba(0, 0, 0, 0.9)"; // 90% 遮罩 + ctx.fill(); + ctx.restore(); + } + }); + }); + } + // 立即启动 + initCanvasEvents(); + // =========================================================== + // Slider 滑块初始化 (保持原样) + // =========================================================== + var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, max: 120, + change: function (value) { + canvas.getActiveObject().set("fontSize", value); + canvas.renderAll(); + } + }); + var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 500, max: 4000, + change: function (value) { + canvas.getActiveObject().set("diameter", value); + canvas.renderAll(); + } + }); + var out_pic_op = slider.render({ + elem: '#out_pic_op', + change: function (value) { + canvas.getActiveObject().set('opacity', (100 - value) / 100); + canvas.renderAll(); + } + }); + var pic_op = slider.render({ + elem: '#pic_op', + change: function (value) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + }); + var rect_op = slider.render({ + elem: '#rect_op', + change: function (value) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + }); + var circle_op = slider.render({ + elem: '#circle_op', + change: function (value) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + }); +// =========================================================== + // 3. 标尺功能 (0点复位 + 拖拽辅助线 + 透明度) + // =========================================================== + function initRulers() { + const dpi = 600; + const mmToPx = dpi / 25.4; + const rulerHeight = 20; + const rulerWidth = 20; + + const canvasWrapper = $('.canvas-wrapper'); + const canvasBox = $('.canvas_box'); + + // 确保容器定位 + if(canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); + + // 初始化尺寸 + let canvasWidth = canvasWrapper.width() || 800; + let canvasHeight = canvasWrapper.height() || 600; + + const $rulerH = $('#ruler-h'); + const $rulerV = $('#ruler-v'); + + $rulerH.attr('width', canvasWidth).attr('height', rulerHeight); + $rulerV.attr('width', rulerWidth).attr('height', canvasHeight); + + // --- 1. 绑定拖拽事件 (核心新功能) --- + // 定义临时拖拽线 DOM + let $dragLine = null; + + // 通用拖拽开始函数 + const startDragGuide = (type, e) => { + e.preventDefault(); + e.stopPropagation(); + + // 创建临时显示的线 + $dragLine = $('
'); + $('.canvas-wrapper').append($dragLine); + + if (type === 'horizontal') { + // 从上往下拖 (水平线) + $dragLine.css({ + left: 0, width: '100%', height: '1px', top: e.offsetY + }); + } else { + // 从左往右拖 (垂直线) + $dragLine.css({ + top: 0, height: '100%', width: '1px', left: e.offsetX + }); + } + + // 绑定 document 移动事件 + $(document).on('mousemove.ruler', function(moveEvent) { + let wrapperOffset = canvasWrapper.offset(); + if (type === 'horizontal') { + let relY = moveEvent.pageY - wrapperOffset.top; + $dragLine.css('top', relY + 'px'); + } else { + let relX = moveEvent.pageX - wrapperOffset.left; + $dragLine.css('left', relX + 'px'); + } + }); + + // 绑定松开事件 (创建真实的 Fabric 线) + $(document).on('mouseup.ruler', function(upEvent) { + $(document).off('mousemove.ruler mouseup.ruler'); + + if (!$dragLine) return; + + // 获取最终位置 + let wrapperOffset = canvasWrapper.offset(); + let finalX = upEvent.pageX - wrapperOffset.left; + let finalY = upEvent.pageY - wrapperOffset.top; + + // 移除临时线 + $dragLine.remove(); + $dragLine = null; + + // 只有拖入画布区域才创建 + if (finalX > rulerWidth && finalY > rulerHeight) { + createFabricGuide(type, finalX, finalY); + } + }); + }; + + // 绑定到标尺 DOM + $rulerH.off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); + $rulerV.off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); + + // --- 2. 在画布中创建辅助线 --- + function createFabricGuide(type, domX, domY) { + // 计算画布内的逻辑坐标 (减去 margin 偏移,除以缩放) + // 注意:这里我们重新回到“原点在画布0,0”的逻辑 + // canvasBox.offset() 是画布容器的绝对位置 + let boxOffset = canvasBox.offset(); + let wrapperOffset = canvasWrapper.offset(); + + // 计算相对于 canvasBox 的像素位置 + let relX = domX - (boxOffset.left - wrapperOffset.left); + let relY = domY - (boxOffset.top - wrapperOffset.top); + + // 转为 Fabric 坐标 (除以 zoom) + let canvasX = relX / zoom; + let canvasY = relY / zoom; + + // 构造线对象 + let linePoints = []; + if (type === 'horizontal') { + // 水平线:贯穿整个画布宽度 + // 为了让线足够长,我们设一个很大的值,或者设为 canvas.width + linePoints = [-5000, canvasY, 5000, canvasY]; + } else { + // 垂直线 + linePoints = [canvasX, -5000, canvasX, 5000]; + } + + let guideLine = new fabric.Line(linePoints, { + stroke: '#00FFFF', // 青色高亮 + strokeWidth: 1 / zoom, // 线条粗细随缩放调整,保持视觉一致 + strokeDashArray: [5, 5], // 虚线 + selectable: true, // 允许选中移动 + evented: true, + hasControls: false, // 没有缩放控制点 + hasBorders: false, + lockRotation: true, // 锁定旋转 + lockScalingX: true, + lockScalingY: true, + hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', + // 自定义属性标记 + isGuideLine: true, + ignoreSave: true // 保存时可以根据这个标记过滤掉 + }); + + // 限制移动方向 + guideLine.on('moving', function(e) { + if (type === 'horizontal') { + this.left = -5000; // 锁定 X 轴不许动 + } else { + this.top = -5000; // 锁定 Y 轴不许动 + } + }); + + canvas.add(guideLine); + canvas.setActiveObject(guideLine); + canvas.renderAll(); + } + + + // --- 3. 绘制标尺 (核心绘制循环) --- + function updateAndDraw() { + const pxPerMm = zoom * mmToPx; + + // 【修改点】原点复位逻辑 + // 现在原点就是 Canvas 的 (0,0) 点 + // 加上 canvasBox 的 margin 偏移量,让标尺 0 刻度对齐画布边缘 + let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; + let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; + + let originX = boxMarginLeft; + let originY = boxMarginTop; + + const ctxH = document.getElementById('ruler-h').getContext('2d'); + const ctxV = document.getElementById('ruler-v').getContext('2d'); + + const drawAxis = (ctx, isH) => { + let len = isH ? canvasWidth : canvasHeight; + ctx.clearRect(0, 0, isH ? len : rulerWidth, isH ? rulerHeight : len); + + // 背景改为透明 (配合 CSS) + // ctx.fillStyle = 'rgba(44, 44, 44, 0.7)'; // 也可以在这里画 + // ctx.fillRect(...) + // 这里我们留空,让 CSS 背景色透出来 + + ctx.strokeStyle = '#999'; // 刻度颜色稍亮一点 + ctx.lineWidth = 1; + ctx.beginPath(); + + // 绘制底边线 + if(isH) { ctx.moveTo(0, 19); ctx.lineTo(len, 19); } + else { ctx.moveTo(19, 0); ctx.lineTo(19, len); } + ctx.stroke(); + + ctx.fillStyle = '#CCC'; + ctx.font = '9px Arial'; + ctx.textAlign = isH ? 'left' : 'center'; + ctx.textBaseline = isH ? 'top' : 'middle'; + ctx.beginPath(); + + // 绘制刻度 (只向正方向绘制) + // 既然原点复位到0了,我们只需要从0开始画 + for (let mm = 0; ; mm += 1) { + let pos = (isH ? originX : originY) + (mm * pxPerMm); + + if (pos > len) break; + + let isMajor = mm % 10 === 0; // 1cm + let isMedium = mm % 5 === 0; // 0.5cm + let tickLen = isMajor ? 15 : (isMedium ? 8 : 4); + + if(isH) { + ctx.moveTo(pos, 20 - tickLen); ctx.lineTo(pos, 20); + if(isMajor) ctx.fillText(mm/10, pos + 2, 2); // 显示 cm 数值 + } else { + ctx.moveTo(20 - tickLen, pos); ctx.lineTo(20, pos); + if(isMajor) { + ctx.save(); + ctx.translate(8, pos + 2); + ctx.rotate(-Math.PI/2); + ctx.fillText(mm/10, 0, 0); + ctx.restore(); + } + } + } + ctx.stroke(); + }; + + drawAxis(ctxH, true); + drawAxis(ctxV, false); + } + + canvas1.on('after:render', updateAndDraw); + canvas2.on('after:render', updateAndDraw); + $(window).resize(() => setTimeout(updateAndDraw, 100)); + updateAndDraw(); + } + + // 辅助函数:更新光标位置 + function updateRulerGuides(nativeEvent) { + if (!nativeEvent) return; + const wrapper = $('.canvas-wrapper'); + const offset = wrapper.offset(); + if (!offset) return; + $("#ruler-guide-h").css({ left: (nativeEvent.pageX - offset.left) + 'px', display: 'block' }); + $("#ruler-guide-v").css({ top: (nativeEvent.pageY - offset.top) + 'px', display: 'block' }); + } + + + + + }) }); \ No newline at end of file diff --git a/frontend-electron/js/design1.js b/frontend-electron/js/design1.js new file mode 100644 index 0000000..811e639 --- /dev/null +++ b/frontend-electron/js/design1.js @@ -0,0 +1,538 @@ +// design1.js - 主入口(桌面端资源路径与 design1-back 一致:../assets/images/) +if (typeof require !== 'undefined') { + try { require('./common/fabric-ext.js'); } catch (e) {} +} +if (typeof window !== 'undefined') { + window.SOON_IMG = '../assets/images/'; + window.soonAsset = function (name) { + return window.SOON_IMG + String(name || '').replace(/^\/+/, ''); + }; +} + +// 过滤 Canvas2D willReadFrequently 警告(不影响功能,只是性能提示) +if (typeof console !== 'undefined' && console.warn) { + const originalWarn = console.warn; + console.warn = function (...args) { + const message = args.join(' '); + // 过滤掉 willReadFrequently 相关的警告 + if (message.includes('willReadFrequently') || message.includes('getImageData')) { + return; // 不输出这个警告 + } + originalWarn.apply(console, args); + }; +} + +// 平台桥由 design1.html 注入 js/platform/electron.js +var remote = typeof window !== 'undefined' ? window.remote : null; +var path = typeof window !== 'undefined' ? window.path : null; +var exePath = typeof window !== 'undefined' ? window.exePath : ''; +var ipcRenderer = typeof window !== 'undefined' ? window.ipcRenderer : null; +var dialog = typeof window !== 'undefined' ? window.dialog : null; +var clipboard = typeof window !== 'undefined' ? window.clipboard : null; +var jrQrcode, JsBarcode; +if (typeof require !== 'undefined') { + try { jrQrcode = require('jr-qrcode'); } catch (e) {} + try { JsBarcode = require('jsbarcode'); } catch (e) {} +} +var dpi = 600; + +if (ipcRenderer) { + ipcRenderer.send('get-sys-fonts'); +} + +// 使用layui + layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { + let myDate = new Date(); + let s_lan = ""; + // 确保 s_lan 在全局作用域中可用(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + if (ipcRenderer) ipcRenderer.send('get-sys-language'); + + var $ = layui.$; + var layer = layui.layer; + var slider = layui.slider; + var form = layui.form; + var colorpicker = layui.colorpicker; + + // 确保所有必要的变量在全局作用域中可用(用于 .jsc 文件加载) + // 在 Electron 的渲染进程中,需要同时设置 window 和 global + if (typeof window !== 'undefined') { + window.$ = $; + window.jQuery = $; + window.ipcRenderer = ipcRenderer; + window.layer = layer; + window.slider = slider; + window.form = form; + window.colorpicker = colorpicker; + window.dialog = dialog; + window.remote = remote; + window.path = path; + window.jrQrcode = jrQrcode; + window.JsBarcode = JsBarcode; + window.clipboard = clipboard; + window.dpi = dpi; + // language_str 会在后面定义,但先确保引用正确 + } + // 在 Node.js 上下文中也设置(require 加载 .jsc 文件时使用) + if (typeof global !== 'undefined') { + global.$ = $; + global.jQuery = $; + global.ipcRenderer = ipcRenderer; + global.layer = layer; + global.slider = slider; + global.form = form; + global.colorpicker = colorpicker; + global.dialog = dialog; + global.remote = remote; + global.path = path; + global.jrQrcode = jrQrcode; + global.JsBarcode = JsBarcode; + global.clipboard = clipboard; + global.dpi = dpi; + } + var layer = layui.layer; + var slider = layui.slider; + var colorpicker = layui.colorpicker; + + // 历史记录文件路径(供 output.js 使用) + var fullPath = (path && exePath) ? path.join(exePath, 'data.json') : (typeof window !== 'undefined' ? window.fullPath : ''); + // 确保 fullPath 在全局作用域中可用(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.fullPath = fullPath; + window.exePath = exePath; + } + if (typeof global !== 'undefined') { + global.fullPath = fullPath; + global.exePath = exePath; + } + + // ========================================== + // 公共函数定义(需要在layui.use回调内部,因为依赖jQuery) + // 注意:所有函数都附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 + // ========================================== + + // DES加密函数 + window.desEncrypt = function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + var APIFMS; + try { + // 确保输入字符串是 UTF-8 编码 + var keyHex = CryptoJS.enc.Utf8.parse(key); + var encrypted = CryptoJS.DES.encrypt(str, keyHex, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + // 使用 Base64 编码确保中文字符正确输出 + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + } catch (err) { + APIFMS = ''; + } + return APIFMS; + }; + // 向后兼容 + function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + return window.desEncrypt(str, key); + } + + // 多语言切换 + window.langua_ge = function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)); + }); + $("[language='t']").each(function (i) { + $(this).attr("title", $(this).attr(lan)); + }); + }; + // 向后兼容 + function langua_ge(lan = 'zh') { + return window.langua_ge(lan); + } + + // 多语言字符串获取 + window.language_str = function language_str(str) { + let t = { + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, + "save": { zh: "保存", ozh: "保存", en: "Save" }, + "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, + "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, + "display": { zh: "预览", ozh: "預覽", en: "Display" }, + "output": { zh: "导出", ozh: "導出", en: "Export" }, + "bg": { zh: "背景", ozh: "背景", en: "Background" }, + "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, + "img": { zh: "圖片", ozh: "圖片", en: "Image" }, + "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, + "text": { zh: "文本", ozh: "文本", en: "Text" }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, + "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, + "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, + "line": { zh: "直線", ozh: "直線", en: "Line" }, + "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, + "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, + "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, + "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, + "vers": { zh: "版本", ozh: "版本", en: "Version" }, + "comf": { zh: "确认", ozh: "確認", en: "Select" }, + "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + }; + // 从全局作用域获取 s_lan(支持 .jsc 文件加载) + const currentLang = (typeof global !== 'undefined' && global.s_lan) ? global.s_lan : + (typeof window !== 'undefined' && window.s_lan) ? window.s_lan : s_lan; + return t[str] ? (t[str][currentLang] || t[str]['zh'] || str) : str; + }; + // 确保 language_str 在 global 作用域中也可用(用于 .jsc 文件加载) + if (typeof global !== 'undefined') { + global.language_str = window.language_str; + } + // 向后兼容 + function language_str(str) { + return window.language_str(str); + } + + // 获取URL参数 + window.GetFile = function GetFile() { + const params = new URLSearchParams(window.location.search); + return params; + }; + // 向后兼容 + function GetFile() { + return window.GetFile(); + } + + // 日期格式化 + window.getDate = function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); + }; + // 向后兼容 + function getDate() { + return window.getDate(); + } + + // 获取对象的绝对坐标 + window.getAbsoluteXY = function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")]; + }; + // 向后兼容 + function getAbsoluteXY(_obj) { + return window.getAbsoluteXY(_obj); + } + + // 计算两点之间的距离 + window.getDisdance = function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); + var dy = Math.abs(y2 - y1); + var distance = Math.sqrt(dx * dx + dy * dy); + return distance; + }; + // 向后兼容 + function getDisdance(x1, y1, x2, y2) { + return window.getDisdance(x1, y1, x2, y2); + } + + // 计算角度(相对于画布中心) + window.getDeg = function getDeg(pointer, canvas) { + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + var angleRad = Math.atan2(dy, dx); + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90; + return angleDeg; + }; + // 向后兼容 + function getDeg(pointer, canvas) { + return window.getDeg(pointer, canvas); + } + + // 检查字段名是否重复 + window.checkName = function checkName(name, objs1, objs2) { + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; + }; + // 向后兼容 + function checkName(name, objs1, objs2) { + return window.checkName(name, objs1, objs2); + } + + // 返回不重复的字段名 + window.resName = function resName(pre_name, objs1, objs2) { + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!window.checkName(pre_name + num, objs1, objs2)); + return pre_name + num; + }; + // 向后兼容 + function resName(pre_name, objs1, objs2) { + return window.resName(pre_name, objs1, objs2); + } + + // 获取对象在画布中的索引 + window.getIndex = function getIndex(target, canvas, _canvas = null) { + let temp_objs; + let useCanvas = (_canvas == null) ? canvas : _canvas; + if (_canvas == null) { + temp_objs = canvas.getObjects(); + } else { + temp_objs = _canvas.getObjects(); + } + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; + }; + // 向后兼容 + function getIndex(target, canvas, _canvas = null) { + return window.getIndex(target, canvas, _canvas); + } + + // 获取对象类型 + window.getType = function getType(target, canvas, objs) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } + }; + // 向后兼容 + function getType(target, canvas, objs) { + return window.getType(target, canvas, objs); + } + + // 获取坐标的最小X值 + window.getCoordsMinX = function getCoordsMinX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x; + } + } + return x; + }; + // 向后兼容 + function getCoordsMinX(acoords) { + return window.getCoordsMinX(acoords); + } + + // 获取坐标的最大X值 + window.getCoordsMaxX = function getCoordsMaxX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x; + } + } + return x; + }; + // 向后兼容 + function getCoordsMaxX(acoords) { + return window.getCoordsMaxX(acoords); + } + + // 获取坐标的最小Y值 + window.getCoordsMinY = function getCoordsMinY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y; + } + } + return y; + }; + // 向后兼容 + function getCoordsMinY(acoords) { + return window.getCoordsMinY(acoords); + } + + // 获取坐标的最大Y值 + window.getCoordsMaxY = function getCoordsMaxY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y; + } + } + return y; + }; + // 向后兼容 + function getCoordsMaxY(acoords) { + return window.getCoordsMaxY(acoords); + } + + // ========================================== + // 全局变量定义(所有模块共享) + // 注意:所有变量都附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 + // ========================================== + window.addState = 0; // 0是不添加 + window.background_image = undefined; + window.background_image1 = undefined; + window.background_image2 = undefined; + window.bg_version = 1; // front_bg1.png front_bg2.png + window.zoom = 1; + window.pre_add_image = undefined; // 预添加的图片对象(用于addPic) + + // 内部剪贴板 + window.clipboardData = { + data: null, + offset: 10 // 粘贴位置偏移量 + }; + + // 文件管理 + window.openAs = { + _name: "", + set name(val) { + if (val == "") { + $("title").html('Soon Design'); + } else { + $("title").html('Soon Design - ' + val); + } + this._name = val; + }, + get name() { + return this._name + }, + }; + + // 对象数组和历史记录 + window.objs1 = []; + window.objs2 = []; + window.step1 = { val: 0 }; + window.step2 = { val: 0 }; + window.step = window.step1; + window.objs = window.objs1; + window.recordJson1 = []; + window.recordJson2 = []; + window.recordJson = window.recordJson1; + window.recordObjs1 = []; + window.recordObjs2 = []; + window.recordObjs = window.recordObjs1; + window.pre_objs1 = []; + window.pre_objs2 = []; + window.next_objs1 = []; + window.next_objs2 = []; + window.pre_objs = window.pre_objs1; + window.next_objs = window.next_objs1; + window.pre_json1 = []; + window.pre_json2 = []; + window.next_json1 = []; + window.next_json2 = []; + window.pre_json = window.pre_json1; + window.next_json = window.next_json1; + + // Canvas对象(将在core.js中初始化) + window.canvas1 = undefined; + window.canvas2 = undefined; + window.canvas = undefined; // 当前活动的画布(canvas1 或 canvas2) + window.ctx1 = undefined; + window.ctx2 = undefined; + window.is_bgi_add = false; + + // 为了兼容性,同时创建局部变量引用(向后兼容) + let addState = window.addState; + let background_image = window.background_image; + let background_image1 = window.background_image1; + let background_image2 = window.background_image2; + let bg_version = window.bg_version; + var zoom = window.zoom; + let pre_add_image = window.pre_add_image; + let clipboardData = window.clipboardData; + var openAs = window.openAs; + let objs1 = window.objs1; + let objs2 = window.objs2; + let step1 = window.step1; + let step2 = window.step2; + let step = window.step; + let objs = window.objs; + let recordJson1 = window.recordJson1; + let recordJson2 = window.recordJson2; + let recordJson = window.recordJson; + let recordObjs1 = window.recordObjs1; + let recordObjs2 = window.recordObjs2; + let recordObjs = window.recordObjs; + let pre_objs1 = window.pre_objs1; + let pre_objs2 = window.pre_objs2; + let next_objs1 = window.next_objs1; + let next_objs2 = window.next_objs2; + let pre_objs = window.pre_objs; + let next_objs = window.next_objs; + let pre_json1 = window.pre_json1; + let pre_json2 = window.pre_json2; + let next_json1 = window.next_json1; + let next_json2 = window.next_json2; + let pre_json = window.pre_json; + let next_json = window.next_json; + var canvas1 = window.canvas1; + var canvas2 = window.canvas2; + var canvas = window.canvas; + var ctx1 = window.ctx1; + var ctx2 = window.ctx2; + let is_bgi_add = window.is_bgi_add; + + // 加载 design1 模块:网页端在回调内动态加载(保证 $ 已存在),桌面端用 require+eval + var fs = typeof window !== 'undefined' ? window.fs : null; + var appPath = (remote && remote.app && remote.app.getAppPath) ? remote.app.getAppPath() : (typeof __dirname !== 'undefined' ? __dirname : '.'); + + if (typeof require === 'undefined') { + // 网页端:按顺序动态插入 script,确保 core/ui 执行时 window.$ 已存在 + function loadScript(src, next) { + var s = document.createElement('script'); + s.src = src; + s.onload = s.onerror = function () { if (typeof next === 'function') next(); }; + document.body.appendChild(s); + } + loadScript('../js/design1/output.js', function () { + loadScript('../js/design1/core.js', function () { + loadScript('../js/design1/ui.js', function () {}); + }); + }); + return; + } + + const loadModule = (moduleName) => { + if (!path || !fs) return; + const jsPath = path.join(appPath, 'js', 'design1', moduleName + '.js'); + if (fs.existsSync(jsPath)) { + try { + eval(fs.readFileSync(jsPath, 'utf8')); + } catch (e) { + alert('加载 ' + moduleName + ' 失败!\n\n错误: ' + e.message); + } + } else { + alert('文件不存在!\n\n请检查:' + jsPath); + } + }; + loadModule('output'); + loadModule('core'); + loadModule('ui'); +}); diff --git a/frontend-electron/js/design1/core.js b/frontend-electron/js/design1/core.js new file mode 100644 index 0000000..ab4e0be --- /dev/null +++ b/frontend-electron/js/design1/core.js @@ -0,0 +1,2330 @@ +// design1核心功能模块 +// 注意:此代码在layui.use回调函数内部执行,不需要$(function())包装 +// 全局变量已在主入口文件中定义,这里只需要初始化 + +$("#object_attribute").css("height", "470px"); +$("#obj_list").css("height", document.body.clientHeight - 520); +$(".obj_list").css("height", $("#obj_list").height() - 32); + +$(".main").show(); +$(".canvas_box").css("height", document.body.clientHeight - 100); +zoom = $(".canvas_bg_img").width() / 2787 +$("#canvas1").attr("width", $(".canvas_box").width() / zoom); +$("#canvas1").attr("height", $(".canvas_box").height() / zoom); +$("#canvas2").attr("width", $(".canvas_box").width() / zoom); +$("#canvas2").attr("height", $(".canvas_box").height() / zoom); + +// 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently +// 注意:这需要在 Fabric.js 创建 canvas 之前执行 +try { + const canvas1El = document.getElementById('canvas1'); + const canvas2El = document.getElementById('canvas2'); + if (canvas1El) { + // 先获取 context 并设置 willReadFrequently,这样 Fabric.js 创建时会复用这个 context + canvas1El.getContext('2d', { willReadFrequently: true }); + } + if (canvas2El) { + canvas2El.getContext('2d', { willReadFrequently: true }); + } +} catch (e) { + // 如果浏览器不支持 willReadFrequently,忽略错误 +} + +// 初始化Canvas对象 +canvas1 = new fabric.Canvas('canvas1'); +canvas2 = new fabric.Canvas('canvas2'); +// 同步到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.canvas1 = canvas1; +window.canvas2 = canvas2; +fabric.Object.prototype.objectCaching = false; + +ctx1 = canvas1.getSelectionContext(); +ctx2 = canvas2.getSelectionContext(); +is_bgi_add = false; +// 撤销/重做过程中使用的标志位,防止并发操作和误记录历史 +window._isApplyingHistory = false; +// 同步到 window 对象 +window.ctx1 = ctx1; +window.ctx2 = ctx2; +window.is_bgi_add = is_bgi_add; +canvas1.zoomToPoint(new fabric.Point(0, 0), 1); +canvas2.zoomToPoint(new fabric.Point(0, 0), 1); + +// 初始化标尺 +initRulers(); + +$("#source_front").width(zoom * $("#source_front").width()); +$("#source_back").width(zoom * $("#source_back").width()); +$(".canvas-container:eq(0)").css('transform', 'scale(' + zoom + ')') +$(".canvas-container:eq(1)").css('transform', 'scale(' + zoom + ')') +$(".canvas-container:eq(0)").css('width', 0) + +$(".canvas-container:eq(1)").hide(); +canvas = canvas1; +window.canvas = canvas; +fabric.Object.prototype.set({ + borderColor: "#9013FE", + cornerColor: "#9013FE", //激活状态角落图标的填充颜色 + cornerStrokeColor: "#9013FE", //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 6, + cornerSize: 24, + cornerStyle: "circle", //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0, +}); +canvas1.preserveObjectStacking = true; +canvas2.preserveObjectStacking = true; + + +var front_list = []; +function applyFontList(data) { + var newFontList = []; + (data || []).map(function(item) { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')); + } else { + newFontList.push(item); + } + }); + front_list = newFontList; + for (var i = 0; i < front_list.length; i++) { + var item = front_list[i]; + $("#text_font_family").append(""); + $("#circle_text_font_family").append(""); + $("#out_text_font_family").append(""); + $("#barcode_font_family").append(""); + $("#count_font_family").append(""); + } +} +if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { + ipcRenderer.on('font-list', function(event, data) { applyFontList(data); }); +} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) { + window.platformBridge.getSystemFonts().then(applyFontList); +} +function applySysLan(data) { + let lang = localStorage.getItem("lang"); + if (lang) { + s_lan = lang; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge(lang); + $("#language_select").val(lang); + return; + } + switch (data) { + case 'zh-CN': + s_lan = "zh"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("zh"); + $("#language_select").val("zh"); + localStorage.setItem("lang", "zh"); + break; + case 'zh': + s_lan = "zh"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("zh"); + $("#language_select").val("zh"); + localStorage.setItem("lang", "zh"); + break; + case 'zh-TW': + s_lan = "ozh"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("ozh"); + $("#language_select").val("ozh"); + localStorage.setItem("lang", "ozh"); + break; + case 'en': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-AU': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-CA': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-GB': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-NZ': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-US': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-ZA': + s_lan = "en"; + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + } +} +if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { + ipcRenderer.on('sys-lan', function(event, data) { applySysLan(data); }); +} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getLocale) { + applySysLan(window.platformBridge.getLocale()); +} +function onCloseConfirm() { + layer.confirm('' + language_str("whetherSave") + '', { + btn: [language_str("save"), language_str("noSave"), language_str("cancel")] + , btn3: function (index, layero) {} + }, function (index, layero) { + if (typeof window.output === 'function') { + window.output(function() { + if (ipcRenderer) ipcRenderer.send('run-close'); + else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose(); + }); + } + }, function (index) { + if (ipcRenderer) ipcRenderer.send('run-close'); + else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose(); + }); +} +if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { + ipcRenderer.on('close', function(event, message) { onCloseConfirm(); }); +} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.onClose) { + window.platformBridge.onClose(onCloseConfirm); +} +addBackground();//添加背景 白色卡片 +initAligningGuidelines(canvas1); +initAligningGuidelines(canvas2); +//canvas.setZoom(0.5) +//recordObjs1.push(JSON.stringify(objs1)); +//recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); +//recordObjs2.push(JSON.stringify(objs2)); +//recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); +canvas.renderAll();//重新渲染画布 + +// 立即启动事件初始化(必须在 slider 初始化之前) +initCanvasEvents(); + +var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + canvas.zoomToPoint( + new fabric.Point(0, 0), + val / 100 + ); + // zoom = val / 100; + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } + +}); +// =========================================================== +// Slider 滑块初始化 +// =========================================================== +var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, max: 120, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set("fontSize", value); + canvas.renderAll(); + } + } +}); +var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 500, max: 4000, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set("diameter", value); + canvas.renderAll(); + } + } +}); +var out_pic_op = slider.render({ + elem: '#out_pic_op', + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', (100 - value) / 100); + canvas.renderAll(); + } + } +}); +var pic_op = slider.render({ + elem: '#pic_op', + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + } +}); +var rect_op = slider.render({ + elem: '#rect_op', + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + } +}); +var circle_op = slider.render({ + elem: '#circle_op', + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + } +}); +window.rotate = function (id) { + let angle = $(`#${id}`).attr("angle") - 0; + angle += 90; + $(`#${id}`).css("transform", `rotate(${angle}deg)`); + $(`#${id}`).attr("angle", angle); +} + +function addBackground() { + // 类型改变 + fabric.Image.fromURL(soonAsset('bg_front_1.png'), function (image) { + let width = $("#canvas1").width() + + image.left = ($("#canvas1").width() - image.width * image.get("scaleX")) / 2; + image.top = ($("#canvas1").height() - image.height * image.get("scaleY")) / 2; + + image.selectable = false; + image.hoverable = false; + image.hoverCursor = "default" + image.setSrc(image.toDataURL(), function (image) { + image.scale(1, 1); + background_image1 = image; + background_image = background_image1; + window.background_image1 = background_image1; + window.background_image = background_image; + canvas.add(image); + canvas.renderAll(); + //pre_objs1.push(JSON.stringify(objs1)); + //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs1.push(JSON.stringify(objs1)); + recordJson1.push(canvas1.toJSON(TO_JSON_PROPERTIES)); + }); + // 类型改变 + fabric.Image.fromURL(soonAsset('bg_back.png'), function (image) { + image.left = ($("#canvas2").width() - image.width) / 2; + image.top = ($("#canvas2").height() - image.height) / 2; + + image.selectable = false; + image.hoverable = false; + image.hoverCursor = "default" + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image; + window.background_image2 = background_image2; + canvas2.add(image); + canvas2.renderAll(); + //pre_objs2.push(JSON.stringify(objs2)); + //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs2.push(JSON.stringify(objs2)); + recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES)); + + let file = GetFile().get('file'); + if (!file && typeof sessionStorage !== 'undefined' && !window.ipcRenderer) { + try { + file = sessionStorage.getItem('soondesign_open_file'); + if (file) { + sessionStorage.removeItem('soondesign_open_file'); + sessionStorage.removeItem('soondesign_open_type'); + } + } catch (e) {} + } + if (file && file != 'empty') { + var p = window.openFile(file) + if (p && p.then) p.then(function() {}, function() {}) + } + }); + }); + is_bgi_add = true; + }); +} + +// 将 updateControls 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.updateControls = function updateControls() { + // 使用 window.canvas 和 window.background_image 作为后备,确保在 eval() 执行时也能访问 + const currentCanvas = window.canvas || canvas; + const currentBackgroundImage = window.background_image || background_image; + if (!currentCanvas || !currentCanvas.getActiveObject()) return; + $("#text_r").val(currentCanvas.getActiveObject().get("angle").toFixed(0)); + $("#text_x").val((currentCanvas.getActiveObject().get("left") - currentBackgroundImage.left).toFixed(0)); + $("#text_y").val((currentCanvas.getActiveObject().get("top") - currentBackgroundImage.top).toFixed(0)); + $("#text_h").val((currentCanvas.getActiveObject().get("scaleY") * currentCanvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((currentCanvas.getActiveObject().get("scaleX") * currentCanvas.getActiveObject().get("width")).toFixed(0)); + // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) + if (currentCanvas.getActiveObject().get("lockMovementX")) { + $("#lock_img").attr("src", soonAsset("lock20.png"))//已锁定 + } else { + $("#lock_img").attr("src", soonAsset("unlock20.png"))//未锁定 + } +}; +// 向后兼容 +function updateControls() { + return window.updateControls(); +} +function selectObject(target, index) { + updateControls(); + + $("#base_control").show(); + //$("#text_x").val((target.left - background_image.left).toFixed(1)); + //$("#text_y").val((target.top - background_image.top).toFixed(1)); + //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); + //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); + //$("#text_r").val(target.get("angle").toFixed(0)); +} + +function addPic(pointer) { + + pre_add_image.left = pointer.x; + pre_add_image.top = pointer.y; + + canvas.add(pre_add_image); + + objs.push({ type: 1, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); + //OpenDialog(); + //function OpenDialog() + //{ + // dialog.showOpenDialog({ + // title: "请选择图片", + // buttonLabel: "确认", + // filters: [ + // { name: 'Image File Type', extensions: ['png', 'jpg'] }, + // ] + // }).then(result => { + // if(result.canceled)return; + // //console.log(result.filePaths) + // fabric.Image.fromURL(result.filePaths[0], function(image) { + // image.left = pointer.x; + // image.top = pointer.y; + // + // canvas.add(image); + // objs.push({type: 1}); + // canvas.renderAll(); + // updateList(); + // window.recordState(); + // } ); + // }).catch(err => { + // console.log(err) + // }) + //} +} + +function selectPic(target, index) { + //透明度 + + $("#pic_control").show(); + $("#set_bg").show(); + let activeObj = canvas.getActiveObject(); + if (typeof pic_op !== 'undefined' && pic_op.setValue) { + pic_op.setValue(100 - (activeObj.get("opacity") * 100)); + } else { + $("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%"); + $("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%"); + } + + let idx = getIndex(canvas.getActiveObject()); + // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + +} + +function addOutPic(pointer) { + fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) { + let width = canvas.getWidth(); + let height = canvas.getHeight(); + image.left = pointer.x; + image.top = pointer.y; + image.opacity = 1; + //image.on("selected",function(){//test well + // console.log(222); + //}); + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image); + objs.push({ type: 2, name: resName("OUT_PIC_"), black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); + }); + }); +} + +function selectOutPic(target, index) { + //字段名 + $("#out_pic_control").show(); + if (typeof out_pic_op !== 'undefined' && out_pic_op.setValue) { + out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#out_pic_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#out_pic_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + let idx = getIndex(canvas.getActiveObject()); + $("#out_pic_field_name").val(objs[idx - 1].name); + // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function addText(pointer) { + let text = new fabric.Text('TEXT', {//源:文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + fontSize: 100, + underline: false, + linethrough: false, + fontFamily: 'Arial' + }); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 3, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectText(target, index) { + //console.log(canvas.getActiveObject().get("fontFamily")); + $("#text_control").show(); + $("#text_color").val(canvas.getActiveObject().get("fill")); + $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#text_size").val(val); + $("#text_text").val(canvas.getActiveObject().get("text")); + $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#text_underline").addClass("ui-radio-acitve") + } else { + $("#text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#text_bold").addClass("ui-radio-acitve") + } else { + $("#text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#text_italics").addClass("ui-radio-acitve") + } else { + $("#text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#text_linethrough").addClass("ui-radio-acitve") + } else { + $("#text_linethrough").removeClass("ui-radio-acitve") + } + $("#circle_text_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + +} + +function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance * 2, + fontSize: 100, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + // originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height / 2 ? true : false + }); + // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) + // text.set({diameter: distance*2}) + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectCircleText() { + // 检查是否有选中对象 + if (!canvas || !canvas.getActiveObject()) return; + + let idx = getIndex(canvas.getActiveObject()); + $("#circle_text_control").show(); + $("#circle_text_color").val(canvas.getActiveObject().get("fill")); + $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + + // 检查 circle_text_size 是否存在且有 setValue 方法 + if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') { + circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); + } else { + $("#circle_text_size").next().children(".layui-slider-bar").css("width", ((canvas.getActiveObject().get("fontSize") - 10 - 10) / (120 - 10)) * 100 + "%"); + $("#circle_text_size").next().children(".layui-slider-handle").css("left", ((canvas.getActiveObject().get("fontSize") - 10 - 10) / (120 - 10)) * 100 + "%"); + } + + // 检查 circle_text_diameter 是否存在且有 setValue 方法 + if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') { + circle_text_diameter.setValue(canvas.getActiveObject().get("diameter") - 500); + } else { + $("#circle_text_diameter").next().children(".layui-slider-bar").css("width", ((canvas.getActiveObject().get("diameter") - 500 - 500) / (4000 - 500)) * 100 + "%"); + $("#circle_text_diameter").next().children(".layui-slider-handle").css("left", ((canvas.getActiveObject().get("diameter") - 500 - 500) / (4000 - 500)) * 100 + "%"); + } + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(canvas.getActiveObject().get("text")); + $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + +} + +function addOutText(pointer) { + //MERGE_TEXT_1 + let text = new fabric.Text("MERGE TEXT", {//源:合成文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + fontStyle: "normal", + fontWeight: "normal", + fontSize: 100, + textBackgroundColor: "", + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }); + + //text.on({ + // 'scaling': function(e) { + // var obj = this, + // w = obj.width * obj.scaleX, + // h = obj.height * obj.scaleY; + // + // obj.set({ + // 'height' : h, + // 'width' : w, + // 'scaleX' : 1, + // 'scaleY' : 1 + // }); + // } + //}); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + + canvas.add(text); + objs.push({ type: 4, name: resName("OUT_TEXT_"), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectOutText(target, index) { + //color + //size + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $("#out_text_control").show(); + $("#out_text_color").val(canvas.getActiveObject().get("fill")); + $("#out_text_text").val(canvas.getActiveObject().get("text")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#out_text_size").val(val); + $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let idx = getIndex(canvas.getActiveObject()); + $("#out_text_field_name").val(objs[idx - 1].name); + $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); + $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); + $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); + if (canvas.getActiveObject().get("underline") == true) { + $("#out_text_underline").addClass("ui-radio-acitve") + } else { + $("#out_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#out_text_bold").addClass("ui-radio-acitve") + } else { + $("#out_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#out_text_italics").addClass("ui-radio-acitve") + } else { + $("#out_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#out_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#out_text_linethrough").removeClass("ui-radio-acitve") + } + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 10, + width: 300, + height: 300 + }); + canvas.add(rect); + objs.push({ type: 5, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectRect(target, index) { + //color + $("#rect_control").show(); + $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + $("#rect_radius").val(canvas.getActiveObject().get("rx")); + if (typeof rect_op !== 'undefined' && rect_op.setValue) { + rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#rect_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#rect_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + let a = canvas.getActiveObject().getCoords(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 10, + radius: 150, + strokeUniform: true, + }); + canvas.add(circle); + objs.push({ type: 6, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectCircle(target, index) { + //border-color + //color + $("#circle_control").show(); + $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + if (typeof circle_op !== 'undefined' && circle_op.setValue) { + circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#circle_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#circle_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function addLine(pointer) { + let line = new fabric.Line([100, 200, 400, 200], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 10, + strokeDashArray: [0, 0], + }); + + line.setControlVisible("tl", false); + line.setControlVisible("tr", false); + line.setControlVisible("br", false); + line.setControlVisible("bl", false); + line.setControlVisible("mt", false); + line.setControlVisible("mb", false); + canvas.add(line); + objs.push({ type: 7, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectLine(target, index) { + //color + //weight + $("#line_control").show(); + + let idx = getIndex(canvas.getActiveObject()); + $("#line_color").val(canvas.getActiveObject().get("stroke")); + $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function addQrCode(pointer) { + + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }); + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x; + image.top = pointer.y; + image.scale(2.4, 2.4) + canvas.add(image); + objs.push({ type: 8, name: resName("QR_CODE_"), color: "#000000", val: "https://www.cardsoon.com", black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); + }); +} + +function selectQrCode(target, index) { + //字段名 + //background-color + //color + $("#qrcode_Controll").show(); + let idx = getIndex(canvas.getActiveObject()); + $("#qrcode_field_name").val(objs[idx - 1].name); + $("#qrcode_color").val(objs[idx - 1].color); + $("#qrcode_val").val(objs[idx - 1].val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#barcode_controll").hide(); +} + +function addBarCode(pointer) { + JsBarcode("#barcode", "123456789", { margin: 0, lineColor: "#000000", fontSize: 18, font: "Arial" }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x; + image.top = pointer.y; + image.scale(2.4, 2.4) + canvas.add(image); + objs.push({ type: 9, name: resName("BAR_CODE_"), val: "123456789", color: "#000000", fontSize: 18, font: "Arial", show: false, black: false });//show是取消文字显示 + canvas.renderAll(); + updateList(); + window.recordState(); + }); +} + +function selectBarCode(target, index) { + //字段名 + //background-color + //color + $("#barcode_controll").show(); + let idx = getIndex(canvas.getActiveObject()); + $("#barcode_field_name").val(objs[idx - 1].name); + $("#barcode_font_family").val(objs[idx - 1].font); + let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#barcode_size").val(val); + $("#barcode_val").val(objs[idx - 1].val); + $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); +} + +function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fontSize: 100, + fill: "#000000", + }); + canvas.add(text); + objs.push({ type: 10, name: resName("COUNTER_"), start: 0, add: 1, black: false }); + canvas.renderAll(); + updateList(); + window.recordState(); +} + +function selectCounter(target, index) { + //起始 + //终止 + //增量 + $("#counter_control").show(); + + + let idx = getIndex(canvas.getActiveObject()); + $("#counter_start").val(objs[idx - 1].start); + $("#counter_add").val(objs[idx - 1].add); + $("#counter_color").val(canvas.getActiveObject().get("fill")); + $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#count_size").val(val); + + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + +} + +// 将 updateList 附加到 window 对象,确保全局可访问 +window.updateList = function updateList() { + let str = ""; + let i = 0; + + const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; + const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; + const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; + + for (let item of objs) { + let displayIndex = i + 1; + + let iconName = "rect.svg"; + switch (item.type) { + case 1: iconName = "images.svg"; break; + case 2: iconName = "images_add.svg"; break; + case 3: iconName = "text.svg"; break; + case 4: iconName = "text2.svg"; break; + case 5: iconName = "rect.svg"; break; + case 6: iconName = "round.svg"; break; + case 7: iconName = "line.svg"; break; + case 8: iconName = "qrcode.svg"; break; + case 9: iconName = "barcode.svg"; break; + case 10: iconName = "counter.svg"; break; + case 11: iconName = "text3.svg"; break; + } + + let nameText = item.name ? item.name : + (item.type === 3 ? "TEXT_" + displayIndex : + (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); + + str = `
+
+ + ${nameText} +
+ ${displayIndex} +
` + str; + i++; + } + $(".obj_list").html(str); + + $(".obj_list > div").on("click", function () { + let imgId = $(this).find("img").attr("id"); + if (imgId) { + selectObj(parseInt(imgId.replace("obj", ""), 10)); + } + }); +} + +// 将 selectObj 附加到 window 对象,确保全局可访问 +// 标记是否通过对象列表选中(用于区分鼠标点击和列表点击) +window._selectingFromList = false; + +window.selectObj = function selectObj(objIndices, isFromCanvas = false) { + let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); + + // 清除所有高亮 + $('.obj_list > div').css("background-color", "transparent"); + + $('.obj_list > div').each(function () { + let $img = $(this).find("img"); + if ($img.length === 0) return; + + let idIndex = parseInt($img.attr("id").replace("obj", ""), 10); + + if (indicesArray.includes(idIndex)) { + $(this).css("background-color", "#6F7A84"); + + // 自动滚动到可视区域 + if (isFromCanvas && indicesArray[0] === idIndex) { + this.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + } + }); + + // 如果是点击列表,触发画布选中 + if (!isFromCanvas && indicesArray.length === 1) { + let target = canvas.getObjects()[indicesArray[0] + 1]; + if (target) { + // 检查对象是否被锁定 + let isLocked = isObjectLocked(target); + + // 设置标志位,表示这是通过对象列表选中的(无论是否锁定) + window._selectingFromList = true; + + // 如果被锁定,临时设置 selectable 和 evented 为 true,以便能够选中 + if (isLocked) { + target.set({ + selectable: true, + evented: true + }); + } + + canvas.setActiveObject(target); + canvas.requestRenderAll(); + + // 如果对象被锁定,选中后立即恢复锁定状态(但保持选中状态) + if (isLocked) { + setTimeout(() => { + target.set({ + selectable: false, + evented: false + }); + canvas.requestRenderAll(); + }, 0); + } + + // 清除标志位(延迟清除,确保事件处理完成) + setTimeout(() => { + window._selectingFromList = false; + }, 10); + + handleObjectSelected(); + } + } +} + +window.recordState = function recordState() { + // 撤销/重做过程中不记录历史,避免产生“中间状态”,导致需要点多次才能回到预期 + if (window._isApplyingHistory) { + return; + } + //pre_objs.push(JSON.stringify(objs)); + //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //next_json = []; + //next_objs = []; + //recordObjs.push(JSON.stringify(objs)); + //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + ++step.val; + for (let i = step.val; i < recordObjs.length; i++) { + recordObjs.pop(); + recordJson.pop(); + } + //recordObjs.splice(step.val, 1, JSON.stringify(objs)) + //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) + recordObjs.push(JSON.stringify(objs)) + j = canvas.toJSON(TO_JSON_PROPERTIES); + j.objects[0].hoverCursor = "default"; + recordJson.push(j); +} + +// 将 getIndex 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.getIndex = function getIndex(target, _canvas = null) { + let temp_objs; + // 使用 window.canvas 作为默认值,确保在 eval() 执行时也能访问 + const currentCanvas = _canvas || window.canvas || canvas; + if (!currentCanvas) { + return 0; + } + temp_objs = currentCanvas.getObjects(); + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; +}; +// 向后兼容 +function getIndex(target, _canvas = null) { + return window.getIndex(target, _canvas); +} + +function getType(target) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } +} + +function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; +} + +function resName(pre_name) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num));//重复了就再加1 + return pre_name + num; +} + +function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; +} + +function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg +} + +function getCoordsMinX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x; +} + +function getCoordsMaxX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x; + +} + +function getCoordsMinY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y; + +} + +function getCoordsMaxY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y; +} + +function handleObjectSelected(e) { + // 确定当前活动的 canvas(从事件参数或全局变量) + let currentCanvas = (e && e.target && (e.target.canvas === canvas1 || e.target.canvas === canvas2)) + ? (e.target.canvas === canvas1 ? canvas1 : canvas2) + : canvas; + + // 如果事件来自 canvas,更新全局 canvas 变量 + if (currentCanvas === canvas1 || currentCanvas === canvas2) { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + window.canvas = canvas; + window.objs = objs; + } + + let select_obj = canvas.getActiveObject(); + + // 1. 未选中任何对象 + if (!select_obj) { + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); // 传 true,只更新UI + return; + } + + // 2. 获取索引 + let i = getIndex(select_obj, canvas); + + // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 + if (!window._selectingFromList && isObjectLocked(select_obj)) { + canvas.discardActiveObject(); + canvas.renderAll(); + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); + return; + } + + if (i != 0) { + // --- 选中了普通对象 --- + --i; // 转换为 objs 数组下标 + + if (objs[i]) { + switch (objs[i].type) { + case 1: // 图片 + $("#component_type").text(language_str("img")); + selectObject(select_obj, i); selectPic(select_obj, i); break; + case 2: // 合成图片 + $("#component_type").text(language_str("simg")); + selectObject(select_obj, i); selectOutPic(select_obj, i); break; + case 3: // 文本 + $("#component_type").text(language_str("text")); + selectObject(select_obj, i); selectText(select_obj, i); break; + case 4: // 合成文本 + $("#component_type").text(language_str("stext")); + selectObject(select_obj, i); selectOutText(select_obj, i); break; + case 5: // 矩形 + $("#component_type").text(language_str("rect")); + selectObject(select_obj, i); selectRect(select_obj, i); break; + case 6: // 圆形 + $("#component_type").text(language_str("circ")); + selectObject(select_obj, i); selectCircle(select_obj, i); break; + case 7: // 直线 + $("#component_type").text(language_str("line")); + selectObject(select_obj, i); selectLine(select_obj, i); break; + case 8: // 二维码 + $("#component_type").text(language_str("qrc")); + selectObject(select_obj, i); selectQrCode(select_obj, i); break; + case 9: // 条形码 + $("#component_type").text(language_str("barc")); + selectObject(select_obj, i); selectBarCode(select_obj, i); break; + case 10: // 计数器 + $("#component_type").text(language_str("counter")); + selectObject(select_obj, i); selectCounter(select_obj, i); break; + case 11: // 圆形文本 + $("#component_type").text(language_str("ctext")); + selectObject(select_obj, i); selectCircleText(select_obj, i); break; + } + } + + // 【关键】这里传入 true! + // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 + selectObj(i, true); + + } else { + // --- 选中了背景 --- + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + selectObj(-1, true); + $("#component_type").text(language_str("bg")); + } +} + +function handleObjectMoving(e) { + let obj = e.target; + let idx = getIndex(obj); + // 圆形文本特殊处理 + if (idx > 0 && idx <= objs.length && objs[idx - 1] && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { x: obj.left, y: obj.top }; + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); + let angleDeg = getDeg(pointer); + let flipped = pointer.y > canvas.height / 2 ? true : false; + + if (typeof circle_text_diameter !== 'undefined') circle_text_diameter.setValue(distance * 2 - 500); + obj.set("diameter", distance * 2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); +} + +// 辅助函数:检查对象是否被锁定 +// 只有当 lockMovementX 为 true 时才认为对象被锁定 +// selectable 为 false 可能是其他原因(如背景图),所以只检查 lockMovementX +function isObjectLocked(obj) { + if (!obj) return false; + // 主要检查 lockMovementX,这是锁定的主要标志 + return obj.get('lockMovementX') === true; +} +// 暴露到全局作用域,确保其他模块可以访问 +window.isObjectLocked = isObjectLocked; + +// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) +// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) +// 包含所有需要保存的属性,特别是自定义对象的属性(如 CurvedText 的 diameter, flipped) +const TO_JSON_PROPERTIES = [ + "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", + "evented", "linethrough", "textBackgroundColor", "diameter", "flipped" +]; +// 暴露到全局作用域 +window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES; + +// 辅助函数:解锁所有对象(加载时使用) +function unlockAllObjects(canvas) { + if (!canvas) return; + const objects = canvas.getObjects(); + objects.forEach((obj, index) => { + // 背景对象(索引0)保持锁定状态 + if (index === 0) { + obj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + } else { + // 其他对象默认解锁 + obj.set({ + lockMovementX: false, + lockMovementY: false, + lockRotation: false, + lockScalingX: false, + lockScalingY: false, + lockSkewingX: false, + lockSkewingY: false, + selectable: true, + evented: true + }); + } + }); +} +// 暴露到全局作用域 +window.unlockAllObjects = unlockAllObjects; + +// 将 handleObjectSelected 附加到 window 对象,确保全局可访问 +window.handleObjectSelected = handleObjectSelected; + +// 防止重复初始化事件监听器 +var _canvasEventsInitialized = false; + +// 在遮罩层之上绘制当前选中对象的边框,避免被遮住 +function drawActiveObjectBorder(currentCanvas, ctx) { + if (!currentCanvas || !ctx) return; + const active = currentCanvas.getActiveObject(); + if (!active) return; + // 辅助线不需要额外描边 + if (active.isGuideLine) return; + + const coords = active.getCoords(true); + if (!coords || coords.length === 0) return; + + ctx.save(); + ctx.beginPath(); + ctx.strokeStyle = '#9013FE'; + // 根据当前缩放比例调整线宽,避免看起来太虚 + let lineWidth = 2; + if (typeof zoom === 'number' && zoom > 0) { + lineWidth = 2 / zoom; // 目标是屏幕上大约 2px 宽 + } + ctx.lineWidth = lineWidth; + // 使用实线边框,提高清晰度 + if (ctx.setLineDash) { + ctx.setLineDash([]); + } + ctx.moveTo(coords[0].x, coords[0].y); + for (let i = 1; i < coords.length; i++) { + ctx.lineTo(coords[i].x, coords[i].y); + } + ctx.closePath(); + ctx.stroke(); + ctx.restore(); +} + +function initCanvasEvents() { + // 如果已经初始化过,直接返回 + if (_canvasEventsInitialized) { + return; + } + _canvasEventsInitialized = true; + + [canvas1, canvas2].forEach(currentCanvas => { + // 1. 鼠标移动 + currentCanvas.on("mouse:move", (e) => { + let offsetX = 0, offsetY = 0; + if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { + offsetX = background_image.left; + offsetY = background_image.top; + } + if (e.pointer) { + $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); + } + updateRulerGuides(e.e); + }); + + // 2. 鼠标移出 + currentCanvas.on("mouse:out", () => { + $("#ruler-guide-h").hide(); $("#ruler-guide-v").hide(); + }); + + // 3. 选中处理 + currentCanvas.on('mouse:down', (e) => { + // 更新全局 canvas 变量 + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + + // 阻止被锁定的对象和辅助线被框选选中: + // - 锁定对象:不能被框选(只能通过列表选中) + // - 辅助线:永远不进入选区,避免选区被拉得很大 + currentCanvas.on('before:selection:created', (e) => { + if (!e.targets || e.targets.length === 0) return; + e.targets = e.targets.filter(target => { + if (target.isGuideLine) return false; // 完全排除辅助线 + return !isObjectLocked(target); // 排除被锁定的普通对象 + }); + }); + + // 添加 selection 事件以确保对象选中时正确显示属性面板 + currentCanvas.on('selection:created', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + currentCanvas.on('selection:updated', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + currentCanvas.on('object:selected', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + + // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 + // 注意:只有真正被锁定的对象才会被取消选中,未锁定的对象应该正常选中 + if (!window._selectingFromList && e.target && isObjectLocked(e.target)) { + currentCanvas.discardActiveObject(); + currentCanvas.renderAll(); + return; + } + + handleObjectSelected(e); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + }); + + // 4. 鼠标松开 (添加对象) + currentCanvas.on("mouse:up", (e) => { + if (addState === 0) return; + const ptr = e.pointer; + switch (addState) { + case 1: addPic(ptr); break; + case 2: addOutPic(ptr); break; + case 3: addText(ptr); break; + case 4: addOutText(ptr); break; + case 5: addRect(ptr); break; + case 6: addCircle(ptr); break; + case 7: addLine(ptr); break; + case 8: addQrCode(ptr); break; + case 9: addBarCode(ptr); break; + case 10: addCounter(ptr); break; + case 11: addCircleText(ptr); break; + } + addState = 0; + if (typeof background_image !== 'undefined') background_image.hoverCursor = "default"; + currentCanvas.renderAll(); + $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); + }); + + // 5. 对象移动 (只保留圆形文本逻辑,去掉了 checkOutOfBounds) + currentCanvas.on("object:moving", (e) => { + // 阻止背景图(索引0)被移动(如果已锁定) + let movingObj = e.target; + let idx = getIndex(movingObj); + if (idx === 0) { + let bgObj = currentCanvas.getObjects()[0]; + if (bgObj && bgObj.get('lockMovementX') === true) { + e.e.preventDefault(); + e.e.stopPropagation(); + return false; + } + } + handleObjectMoving(e); + }); + + // 6. 变换事件 + currentCanvas.on("object:rotating", updateControls); + currentCanvas.on("object:scaling", updateControls); + currentCanvas.on("object:rotated", () => { window.recordState(); }); + currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); + currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); + currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); + + // 7. 对象修改后 + currentCanvas.on('object:modified', function (event) { + if (currentCanvas.getActiveObjects().length > 1) return; + let idx = getIndex(event.target); + if (idx <= 0) return; + let objData = objs[idx - 1]; + // 安全检查:确保 objData 存在且有 type 属性 + if (!objData || typeof objData.type === 'undefined') { + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + return; + } + let target = event.target; + + if ([3, 4, 10].includes(objData.type)) { + target.fontSize = (target.fontSize * target.scaleX).toFixed(0); + target.scaleX = 1; target.scaleY = 1; + target._clearCache(); + if (objData.type == 3) $("#text_size").val(target.fontSize); + if (objData.type == 4) $("#out_text_size").val(target.fontSize); + if (objData.type == 10) $("#count_size").val(target.fontSize); + currentCanvas.renderAll(); + updateControls(); + } else if (objData.type == 5) { + target.width *= target.scaleX; target.height *= target.scaleY; + target.scaleX = 1; target.scaleY = 1; + updateControls(); + } + window.recordState(); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + }); + // ================================================== + // 8. 【完美修复版】渲染后绘制:外部蒙版 + 中心圆孔 (修复三角形Bug) + // ================================================== + currentCanvas.on('after:render', function () { + const ctx = currentCanvas.getContext(); + let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2; + + if (bgObj) { + const width = currentCanvas.width; + const height = currentCanvas.height; + + // 1. 计算圆心 + const cx = bgObj.left + (bgObj.width * bgObj.scaleX) / 2; + const cy = bgObj.top + (bgObj.height * bgObj.scaleY) / 2; + + // 2. 计算半径 + // 外半径 (光盘大小) + const r_outer = (bgObj.width * bgObj.scaleX) / 2; + + // 内半径 (中心孔) + const dpi = 600; + const mmToPx = dpi / 25.4; + let holeDiameterMm = 24; // 默认 15mm + + // 切换版本判断 (15mm vs 38mm) + if (currentCanvas === canvas1 && typeof bg_version !== 'undefined' && bg_version === 2) { + holeDiameterMm = 42; + } + + // 比例计算法:确保孔径随缩放适配 + // r_inner = r_outer * (孔径 / 光盘标准直径120mm) + const r_inner = r_outer * (holeDiameterMm / 120); + + ctx.save(); + ctx.beginPath(); + + // A. 绘制全屏矩形 (顺时针) + ctx.rect(0, 0, width, height); + + // B. 挖掉外圆 (逆时针) + // 【关键修复】画圆前,必须把笔移动到圆的起点 (cx + r, cy) + // 否则 Canvas 会画一条线连过来,形成奇怪的三角形 + ctx.moveTo(cx + r_outer, cy); + ctx.arc(cx, cy, r_outer, 0, Math.PI * 2, true); + + // C. 填补内圆 (顺时针) + // 【关键修复】同理,把笔移动到内圆的起点 + ctx.moveTo(cx + r_inner, cy); + ctx.arc(cx, cy, r_inner, 0, Math.PI * 2, false); + + ctx.closePath(); + ctx.fillStyle = "rgba(0, 0, 0, 0.9)"; // 90% 遮罩 + ctx.fill(); + ctx.restore(); + } + + // 在遮罩之后重新绘制辅助线,确保辅助线在最上层 + const objects = currentCanvas.getObjects(); + objects.forEach(obj => { + if (obj.isGuideLine) { + ctx.save(); + // 应用对象的变换矩阵 + const m = obj.calcTransformMatrix(); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + + // 设置透明度 + ctx.globalAlpha = obj.opacity || 1; + + // 应用阴影 + if (obj.shadow) { + ctx.shadowColor = obj.shadow.color; + ctx.shadowBlur = obj.shadow.blur; + ctx.shadowOffsetX = obj.shadow.offsetX; + ctx.shadowOffsetY = obj.shadow.offsetY; + } + + // 使用 Fabric 对象的内置渲染方法 + obj._render(ctx); + ctx.restore(); + } + }); + + // 最后再补绘当前选中对象的边框,确保不会被遮罩层挡住 + drawActiveObjectBorder(currentCanvas, ctx); + }); + }); +} + +function initRulers() { + const dpi = 600; + const mmToPx = dpi / 25.4; + const rulerHeight = 20; + const rulerWidth = 20; + + const canvasWrapper = $('.canvas-wrapper'); + const canvasBox = $('.canvas_box'); + + // 确保容器定位 + if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); + + // 初始化尺寸 + let canvasWidth = canvasWrapper.width() || 800; + let canvasHeight = canvasWrapper.height() || 600; + + const $rulerH = $('#ruler-h'); + const $rulerV = $('#ruler-v'); + + $rulerH.attr('width', canvasWidth).attr('height', rulerHeight); + $rulerV.attr('width', rulerWidth).attr('height', canvasHeight); + + // --- 1. 绑定拖拽事件 (核心新功能) --- + // 定义临时拖拽线 DOM + let $dragLine = null; + + // 通用拖拽开始函数 + const startDragGuide = (type, e) => { + e.preventDefault(); + e.stopPropagation(); + + // 防止重复调用:如果正在拖拽,直接返回 + if ($dragLine && $dragLine.length > 0) { + return; + } + + // 创建临时显示的线 + $dragLine = $('
'); + $('.canvas-wrapper').append($dragLine); + + if (type === 'horizontal') { + // 从上往下拖 (水平线) + $dragLine.css({ + left: 0, width: '100%', height: '1px', top: e.offsetY + }); + } else { + // 从左往右拖 (垂直线) + $dragLine.css({ + top: 0, height: '100%', width: '1px', left: e.offsetX + }); + } + + // 绑定 document 移动事件 + $(document).on('mousemove.ruler', function (moveEvent) { + let wrapperOffset = canvasWrapper.offset(); + if (type === 'horizontal') { + let relY = moveEvent.pageY - wrapperOffset.top; + $dragLine.css('top', relY + 'px'); + } else { + let relX = moveEvent.pageX - wrapperOffset.left; + $dragLine.css('left', relX + 'px'); + } + }); + + // 绑定松开事件 (创建真实的 Fabric 线) + $(document).on('mouseup.ruler', function (upEvent) { + $(document).off('mousemove.ruler mouseup.ruler'); + + if (!$dragLine) return; + + // 获取最终位置 + let wrapperOffset = canvasWrapper.offset(); + let finalX = upEvent.pageX - wrapperOffset.left; + let finalY = upEvent.pageY - wrapperOffset.top; + + // 移除临时线 + $dragLine.remove(); + $dragLine = null; + + // 只有拖入画布区域才创建 + if (finalX > rulerWidth && finalY > rulerHeight) { + createFabricGuide(type, finalX, finalY); + } + }); + }; + + // 绑定到标尺 DOM + $rulerH.off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); + $rulerV.off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); + + // --- 2. 在画布中创建辅助线 --- + function createFabricGuide(type, domX, domY) { + // domX 和 domY 是相对于 canvas-wrapper 的坐标 + // 需要转换为相对于 canvas_box 的坐标,然后再转换为 Fabric 画布坐标 + + // 获取 canvas_box 相对于 canvas-wrapper 的位置 + let boxPosition = canvasBox.position(); + let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; + let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; + + // 计算相对于 canvas_box 的坐标(减去 box 在 wrapper 中的位置和 margin) + let relX = domX - boxPosition.left - boxMarginLeft; + let relY = domY - boxPosition.top - boxMarginTop; + + // 转为 Fabric 坐标 (除以 zoom) + let canvasX = relX / zoom; + let canvasY = relY / zoom; + + // 获取画布的实际尺寸(用于决定辅助线的长度) + let canvasWidth = canvas.width || 2000; + let canvasHeight = canvas.height || 2000; + // 使用与画布尺寸相关的长度,避免“整块区域都像辅助线”,同时保证能横穿整个画布 + let lineExtent = Math.max(canvasWidth, canvasHeight) * 2; + + // 构造线对象 + let linePoints = []; + if (type === 'horizontal') { + // 水平线:从很远的左边到很远的右边 + linePoints = [-lineExtent, canvasY, lineExtent, canvasY]; + } else { + // 垂直线:从很远的上面到很远的下面 + linePoints = [canvasX, -lineExtent, canvasX, lineExtent]; + } + + let guideLine = new fabric.Line(linePoints, { + stroke: '#0066FF', + strokeWidth: 1 / zoom, + opacity: 0.9, + shadow: { + color: '#0066FF', + blur: 6, + offsetX: 0, + offsetY: 0, + affectStroke: true + }, + selectable: true, + evented: true, + hasControls: false, + hasBorders: false, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', + // design1 中改回边框命中模型,保证更容易点中拖拽 + perPixelTargetFind: false, + targetFindTolerance: 10, + padding: 10, + isGuideLine: true, + ignoreSave: true + }); + + // 限制移动方向(只约束方向,不把整条线拖到极远,避免“整片区域是辅助线”的感觉) + guideLine.on('moving', function () { + if (type === 'horizontal') { + // 水平线:根据当前中心点的 Y,重算 x1/x2,使其横穿画布 + const cy = this.getCenterPoint().y; + this.set({ + x1: -lineExtent, + y1: cy, + x2: lineExtent, + y2: cy + }); + } else { + // 垂直线:根据当前中心点的 X,重算 y1/y2,使其竖穿画布 + const cx = this.getCenterPoint().x; + this.set({ + x1: cx, + y1: -lineExtent, + x2: cx, + y2: lineExtent + }); + } + }); + + canvas.add(guideLine); + canvas.setActiveObject(guideLine); + canvas.renderAll(); + } + + // --- 3. 绘制标尺 (核心绘制循环) --- + function updateAndDraw() { + // 每次重新获取尺寸,确保使用最新值 + // 使用多种方法尝试获取实际尺寸,确保准确 + let currentCanvasWidth = canvasWrapper.width() || canvasWrapper.outerWidth() || canvasWrapper[0].getBoundingClientRect().width || 800; + let currentCanvasHeight = canvasWrapper.height() || canvasWrapper.outerHeight() || canvasWrapper[0].getBoundingClientRect().height || 600; + + // 如果还是获取不到,尝试使用父容器的尺寸 + if (!currentCanvasWidth || currentCanvasWidth === 0) { + const parent = canvasWrapper.parent(); + currentCanvasWidth = parent.width() || parent[0].getBoundingClientRect().width || 800; + } + if (!currentCanvasHeight || currentCanvasHeight === 0) { + const parent = canvasWrapper.parent(); + currentCanvasHeight = parent.height() || parent[0].getBoundingClientRect().height || 600; + } + + // 更新标尺尺寸 + $rulerH.attr('width', currentCanvasWidth).attr('height', rulerHeight); + $rulerV.attr('width', rulerWidth).attr('height', currentCanvasHeight); + + const pxPerMm = zoom * mmToPx; + + let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; + let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; + const rulerHeightOffset = 20; // ruler 的高度 + + // 原点在标尺的左上角(画布的左上角位置,考虑margin) + // 标尺0点 = boxMarginLeft(水平)和 boxMarginTop + rulerHeightOffset(垂直) + let originX = boxMarginLeft; + let originY = boxMarginTop + rulerHeightOffset; + + const ctxH = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true }); + const ctxV = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true }); + + // 绘制水平标尺 + function drawHorizontal() { + ctxH.clearRect(0, 0, currentCanvasWidth, rulerHeight); + + ctxH.strokeStyle = '#666'; + ctxH.lineWidth = 1; + ctxH.beginPath(); + ctxH.moveTo(0, 19); + ctxH.lineTo(currentCanvasWidth, 19); + ctxH.stroke(); + + ctxH.fillStyle = '#888'; + ctxH.font = '9px Arial'; + ctxH.textAlign = 'left'; + ctxH.textBaseline = 'top'; + + const startX = 0; + const endX = currentCanvasWidth; + + // 绘制主要刻度(每10mm) + // 从0点向左绘制负值刻度,确保覆盖到startX + for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) { + const x = originX + mm * pxPerMm; + + ctxH.beginPath(); + ctxH.moveTo(x, 5); + ctxH.lineTo(x, 20); + ctxH.stroke(); + + ctxH.fillText(mm + 'mm', x + 2, 2); + } + // 从0点向右绘制正值刻度,确保覆盖到endX + for (let mm = 0; ; mm += 10) { + const x = originX + mm * pxPerMm; + if (x > endX) break; + + ctxH.beginPath(); + ctxH.moveTo(x, 5); + ctxH.lineTo(x, 20); + ctxH.stroke(); + + ctxH.fillText(mm + 'mm', x + 2, 2); + } + + // 绘制次要刻度(每1mm) + // 从0点向左绘制负值刻度,确保覆盖到startX + for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) { + const x = originX + mm * pxPerMm; + if (mm % 10 === 0) continue; + + ctxH.beginPath(); + ctxH.moveTo(x, 12); + ctxH.lineTo(x, 20); + ctxH.stroke(); + } + // 从0点向右绘制正值刻度,确保覆盖到endX + for (let mm = 0; ; mm += 1) { + const x = originX + mm * pxPerMm; + if (x > endX) break; + if (mm % 10 === 0) continue; + + ctxH.beginPath(); + ctxH.moveTo(x, 12); + ctxH.lineTo(x, 20); + ctxH.stroke(); + } + } + + // 绘制垂直标尺 + function drawVertical() { + ctxV.clearRect(0, 0, rulerWidth, currentCanvasHeight); + + ctxV.strokeStyle = '#666'; + ctxV.lineWidth = 1; + ctxV.beginPath(); + ctxV.moveTo(19, 0); + ctxV.lineTo(19, currentCanvasHeight); + ctxV.stroke(); + + ctxV.fillStyle = '#888'; + ctxV.font = '9px Arial'; + ctxV.textAlign = 'center'; + ctxV.textBaseline = 'middle'; + + const startY = 0; + const endY = currentCanvasHeight; + + // 绘制主要刻度(每10mm) + // 从0点向上绘制负值刻度,确保覆盖到startY + for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) { + const y = originY + mm * pxPerMm; + + ctxV.beginPath(); + ctxV.moveTo(5, y); + ctxV.lineTo(20, y); + ctxV.stroke(); + + ctxV.save(); + ctxV.translate(8, y + 2); + ctxV.rotate(-Math.PI / 2); + ctxV.fillText(mm + 'mm', 0, 0); + ctxV.restore(); + } + // 从0点向下绘制正值刻度,确保覆盖到endY + for (let mm = 0; ; mm += 10) { + const y = originY + mm * pxPerMm; + if (y > endY) break; + + ctxV.beginPath(); + ctxV.moveTo(5, y); + ctxV.lineTo(20, y); + ctxV.stroke(); + + ctxV.save(); + ctxV.translate(8, y + 2); + ctxV.rotate(-Math.PI / 2); + ctxV.fillText(mm + 'mm', 0, 0); + ctxV.restore(); + } + + // 绘制次要刻度(每1mm) + // 从0点向上绘制负值刻度,确保覆盖到startY + for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) { + const y = originY + mm * pxPerMm; + if (mm % 10 === 0) continue; + + ctxV.beginPath(); + ctxV.moveTo(12, y); + ctxV.lineTo(20, y); + ctxV.stroke(); + } + // 从0点向下绘制正值刻度,确保覆盖到endY + for (let mm = 0; ; mm += 1) { + const y = originY + mm * pxPerMm; + if (y > endY) break; + if (mm % 10 === 0) continue; + + ctxV.beginPath(); + ctxV.moveTo(12, y); + ctxV.lineTo(20, y); + ctxV.stroke(); + } + } + + drawHorizontal(); + drawVertical(); + } + + canvas1.on('after:render', updateAndDraw); + canvas2.on('after:render', updateAndDraw); + $(window).resize(() => setTimeout(updateAndDraw, 100)); + updateAndDraw(); +} + +// 辅助函数:更新光标位置 +function updateRulerGuides(nativeEvent) { + if (!nativeEvent) return; + const wrapper = $('.canvas-wrapper'); + const offset = wrapper.offset(); + if (!offset) return; + $("#ruler-guide-h").css({ left: (nativeEvent.pageX - offset.left) + 'px', display: 'block' }); + $("#ruler-guide-v").css({ top: (nativeEvent.pageY - offset.top) + 'px', display: 'block' }); +} + +window.copySelection = function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + layer.msg(language_str("selectPic") || "请先选择对象"); + return; + } + + // 【核心修复】 + // 遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 + clipboardData.data = activeObjects.map(obj => { + let idx = getIndex(obj); // 获取对象在画布中的索引 + + // 获取业务属性 (objs数组中对应的那一项) + // 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1 + let appData = (idx > 0 && objs[idx - 1]) ? objs[idx - 1] : { type: 0 }; + + return { + // 1. 保存画面样式 (坐标、颜色、字号等,包含图片的 src) + fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data', 'src']), + // 2. 保存业务属性 (深拷贝,防止引用冲突) + appData: JSON.parse(JSON.stringify(appData)) + }; + }); + + clipboardData.offset = 10; // 重置偏移量 + layer.msg("已复制"); +} + +window.pasteFromClipboard = function pasteFromClipboard() { + // 检查是否有数据 + if (!clipboardData.data || clipboardData.data.length === 0) { + // layer.msg("剪贴板为空"); + return; + } + + // 1. 提取 Fabric JSON 准备恢复画面 + const jsonToEnliven = clipboardData.data.map(item => item.fabricJson); + + // 2. 恢复对象 (异步过程) + fabric.util.enlivenObjects(jsonToEnliven, function (objects) { + canvas.discardActiveObject(); + + objects.forEach((clone, index) => { + // --- A. 处理画面位置 --- + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + + // 如果是组内对象,需要特殊处理坐标 + if (clone.group) { + clone.set({ + left: clone.group.left + clipboardData.offset, + top: clone.group.top + clipboardData.offset + }); + } + + // --- B. 处理业务数据 --- + // 取出复制时保存的 appData + let savedAppData = clipboardData.data[index].appData; + let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝 + + // 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy) + if (newAppData.name) { + newAppData.name = newAppData.name + "_copy"; + } + + // --- C. 添加到画布和数据数组 --- + canvas.add(clone); + objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组 + }); + + // 3. 选中新粘贴出来的对象 + let sel = new fabric.ActiveSelection(objects, { canvas: canvas }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + + // 4. 刷新列表和历史记录 + updateList(); + window.recordState(); + + // 5. 高亮左侧列表最后一项 (即新粘贴的项) + if (objs.length > 0) selectObj(objs.length - 1); + + // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) + clipboardData.offset += 10; + }); +} + +// 将 onStateRestored 附加到 window 对象,确保全局可访问 +window.onStateRestored = function onStateRestored() { + // 1. 重新获取背景图引用 (因为旧对象被销毁了) + // 注意:canvas 是全局变量,指向当前激活的画板 + if (canvas.getObjects().length > 0) { + background_image = canvas.getObjects()[0]; + background_image.selectable = false; // 确保背景不可选 + window.background_image = background_image; + background_image.evented = false; + } + + // 2. 渲染画布 + canvas.requestRenderAll(); + + // 3. 取消选中状态 + if (typeof window.selectObj === 'function') { + window.selectObj(-1); + } + + // 4. 隐藏所有属性面板 + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + + // 5. 刷新左侧列表 + if (typeof window.updateList === 'function') { + window.updateList(); + } +} diff --git a/lib/design1/output.js b/frontend-electron/js/design1/output.js similarity index 98% rename from lib/design1/output.js rename to frontend-electron/js/design1/output.js index 94c948a..ce9aea6 100644 --- a/lib/design1/output.js +++ b/frontend-electron/js/design1/output.js @@ -408,9 +408,9 @@ window.display_func = function display_func(img1, img2, img3) {
- +
- +
@@ -424,7 +424,7 @@ window.display_func = function display_func(img1, img2, img3) { var blob = buffer instanceof Uint8Array ? new Blob([buffer], { type: 'image/png' }) : new Blob([buffer], { type: 'image/png' }); window.platformBridge.printPdf(blob); } else if (typeof printJS === 'function') { - printJS({ printable: printPath, type: 'image', style: 'img { width: 100%; height: auto; }' }); + printJS({ printable: url3, type: 'image', style: 'img { width: 100%; height: auto; }' }); } }, end: function () { @@ -455,11 +455,11 @@ window.display_func = function display_func(img1, img2, img3) { // 将 output 附加到 window 对象,确保全局可访问 window.output = function output(callback = null, _save = save) { // 类型改变 - fabric.Image.fromURL('./public/images/op_1.png', function (i1) { + fabric.Image.fromURL(soonAsset('op_1.png'), function (i1) { i1.left = background_image.left; i1.top = background_image.top; let img1 = i1; - fabric.Image.fromURL('./public/images/op_1.png', function (i2) { + fabric.Image.fromURL(soonAsset('op_1.png'), function (i2) { i2.left = background_image.left; i2.top = background_image.top; let img2 = i2; @@ -1093,17 +1093,17 @@ window.openFile = function open(file, jAlready) { j.f.objects[i].left = j.f.objects[i].left - new_left + left; j.f.objects[i].top = j.f.objects[i].top - new_top + top; } - if (j.f.objects[0] && !j.f.objects[0].src) j.f.objects[0].src = './public/images/bg_front_1.png'; - if (j.b.objects && j.b.objects[0] && !j.b.objects[0].src) j.b.objects[0].src = './public/images/bg_back.png'; + if (j.f.objects[0] && !j.f.objects[0].src) j.f.objects[0].src = soonAsset('bg_front_1.png'); + if (j.b.objects && j.b.objects[0] && !j.b.objects[0].src) j.b.objects[0].src = soonAsset('bg_back.png'); var fo = j.fo || [], bo = j.bo || []; var placeholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; fo.forEach(function(objMeta, idx) { if ((objMeta.type === 8 || objMeta.type === 9) && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) j.f.objects[idx + 1].src = placeholder; - else if (objMeta.type === 2 && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) j.f.objects[idx + 1].src = './public/images/outpic_extra.png'; + else if (objMeta.type === 2 && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) j.f.objects[idx + 1].src = soonAsset('outpic_extra.png'); }); bo.forEach(function(objMeta, idx) { if ((objMeta.type === 8 || objMeta.type === 9) && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) j.b.objects[idx + 1].src = placeholder; - else if (objMeta.type === 2 && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) j.b.objects[idx + 1].src = './public/images/outpic_extra.png'; + else if (objMeta.type === 2 && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) j.b.objects[idx + 1].src = soonAsset('outpic_extra.png'); }); canvas1.loadFromJSON(j.f, function() { if (typeof window.unlockAllObjects === 'function') window.unlockAllObjects(canvas1); @@ -1163,6 +1163,7 @@ window.openFile = function open(file, jAlready) { openAs.name = file; return; } + file = resolveDiskPath(file); if (window.platformBridge && window.platformBridge.readJsonFile) { return window.platformBridge.readJsonFile(file).then(function(j) { if (j) { @@ -1173,7 +1174,7 @@ window.openFile = function open(file, jAlready) { } try { var fs = typeof require !== 'undefined' ? require('fs') : null; - if (!fs || file.indexOf('soondesign_session:') === 0) return; + if (!fs || (isWebPortal() && file.indexOf('soondesign_session:') === 0)) return; if (!fs.existsSync(file)) return; var fsData = fs.readFileSync(file); var j; @@ -1216,6 +1217,16 @@ function cleanupSrcFields(jsonObjects, objsArray) { return window.cleanupSrcFields(jsonObjects, objsArray); } +function isWebPortal() { + return !!(window.platformBridge && !window.ipcRenderer); +} +function resolveDiskPath(p) { + if (!p) return p; + var prefix = 'soondesign_session:'; + if (!isWebPortal() && p.indexOf(prefix) === 0) return p.substring(prefix.length); + return p; +} + function saveAs(op1, callback) { // 临时移除辅助线,保存后再恢复 let guideLines1 = []; @@ -1264,9 +1275,9 @@ function saveAs(op1, callback) { con_o.soonType = 1; if (window.platformBridge && window.platformBridge.writeFile) { await window.platformBridge.writeFile(result.fileHandle || fp, content); - openAs.name = 'soondesign_session:' + fp; - webSessionStore(content, fp); - layer.msg(language_str("saveSucc") + fp); + openAs.name = isWebPortal() ? ('soondesign_session:' + fp) : (result.filePath || fp); + webSessionStore(content, fp); + layer.msg(language_str("saveSucc") + (isWebPortal() ? fp : openAs.name)); saveHistory(); if (callback && typeof callback === 'function') callback(); return; @@ -1318,7 +1329,7 @@ function save(op1, callback) { if (openAs.name != "") { con_o.soonType = 1; var content = JSON.stringify(con_o); - if (openAs.name.indexOf('soondesign_session:') === 0) { + if (isWebPortal() && openAs.name.indexOf('soondesign_session:') === 0) { webSessionStore(content, openAs.name); // 服务器环境:同时保存到服务器 var fileName = openAs.name.replace(/^soondesign_session:/, ''); @@ -1363,9 +1374,9 @@ function save(op1, callback) { con_o.soonType = 1; if (window.platformBridge && window.platformBridge.writeFile) { await window.platformBridge.writeFile(result.fileHandle || fp, content); - openAs.name = 'soondesign_session:' + fp; - webSessionStore(content, fp); - layer.msg(language_str("saveSucc") + fp); + openAs.name = isWebPortal() ? ('soondesign_session:' + fp) : (result.filePath || fp); + webSessionStore(content, fp); + layer.msg(language_str("saveSucc") + (isWebPortal() ? fp : openAs.name)); saveHistory(); if (callback && typeof callback === 'function') callback(); return; @@ -1383,7 +1394,7 @@ function save(op1, callback) { // 网页端:保存前把当前内容写入 sessionStorage 与 localStorage,供首页历史读取(localStorage 跨标签/重开可用) function webSessionStore(content, fileName) { - if (!window.platformBridge || typeof sessionStorage === 'undefined') return; + if (!isWebPortal() || !window.platformBridge || typeof sessionStorage === 'undefined') return; var name = fileName || openAs.name || 'design.soon'; var key = (name && name.indexOf('soondesign_session:') === 0) ? name : ('soondesign_session:' + name); try { @@ -1395,9 +1406,9 @@ function webSessionStore(content, fileName) { // 将 saveHistory 附加到 window 对象,确保在 ui.js 中也能访问 window.saveHistory = function saveHistory() { function doWrite(j) { - var currentPath = openAs.name; + var currentPath = resolveDiskPath(openAs.name); var pathForHistory = currentPath; - if (window.platformBridge && (currentPath.indexOf('soondesign_session:') !== 0)) { + if (isWebPortal() && currentPath.indexOf('soondesign_session:') !== 0) { pathForHistory = 'soondesign_session:' + (currentPath || 'design.soon'); } var newList = (j.history || []).filter(function(item) { return item.path !== pathForHistory && item.path !== currentPath; }); @@ -1405,20 +1416,19 @@ window.saveHistory = function saveHistory() { if (newList.length > 20) newList = newList.slice(0, 20); j.history = newList; if (window.platformBridge && window.platformBridge.writeHistory) { - window.platformBridge.writeHistory(j); - return; + return window.platformBridge.writeHistory(j); } var fs = typeof require !== 'undefined' ? require('fs') : null; if (fs && fullPath) { try { fs.writeFileSync(fullPath, JSON.stringify(j), 'utf8'); } catch (e) {} } + return Promise.resolve(); } if (window.platformBridge && window.platformBridge.readHistory) { - window.platformBridge.readHistory().then(function(j) { + return window.platformBridge.readHistory().then(function(j) { if (!j || !Array.isArray(j.history)) j = { history: [] }; - doWrite(j); - }).catch(function() { doWrite({ history: [] }); }); - return; + return doWrite(j); + }).catch(function() { return doWrite({ history: [] }); }); } var fs = typeof require !== 'undefined' ? require('fs') : null; var j = { history: [] }; @@ -1431,7 +1441,7 @@ window.saveHistory = function saveHistory() { } catch (e) { j = { history: [] }; } } if (!Array.isArray(j.history)) j.history = []; - doWrite(j); + return doWrite(j); }; // 向后兼容 function saveHistory() { diff --git a/lib/design1/ui.js b/frontend-electron/js/design1/ui.js similarity index 95% rename from lib/design1/ui.js rename to frontend-electron/js/design1/ui.js index 961d4b7..8ae8a07 100644 --- a/lib/design1/ui.js +++ b/frontend-electron/js/design1/ui.js @@ -1,2480 +1,2436 @@ -// 移至上层和移至下层的事件处理在下方(第 2179 行),避免重复绑定 - -var _keyctrl = false, - _keyc = false, - _textEdit = false, - _inputEdit = false; -$('input').focus(function () { - _inputEdit = true; -}); -$('input').blur(function () { - _inputEdit = false; -}); -$('textarea').focus(function () { - _inputEdit = true; -}); -$('textarea').blur(function () { - _inputEdit = false; -}); -$("body").keyup(function (event) { - // 兼容三端�?modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta�? - if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释�? - _keyctrl = false; - }; - if (event.keyCode == 67) { - _keyc = false; - }; -}) -$("body").keydown(function (event) { - // 统一的快捷键处理(兼�?Windows/Linux: Ctrl, Mac: Cmd/Meta�? - const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 - - // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的�? - if (!_inputEdit && !_textEdit) { - // CTRL/CMD+N - 新建 - if (isModifierKey && event.keyCode == 78) { - event.preventDefault(); - $("#new").click(); - return; - } - // CTRL/CMD+O - 打开 - if (isModifierKey && event.keyCode == 79) { - event.preventDefault(); - $("#open").click(); - return; - } - // CTRL/CMD+S - 保存 - if (isModifierKey && !event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - $("#save").click(); - return; - } - // CTRL/CMD+SHIFT+S - 保存副本 - if (isModifierKey && event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - if (typeof window.output === 'function') { - window.output(null, saveAs); - } - return; - } - // CTRL/CMD+Z - 还原 - if (isModifierKey && !event.shiftKey && event.keyCode == 90) { - event.preventDefault(); - $("#previous").click(); - return; - } - // CTRL/CMD+Y - 重做 - if (isModifierKey && !event.shiftKey && event.keyCode == 89) { - event.preventDefault(); - $("#next").click(); - return; - } - // CTRL/CMD+C - 复制 - if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { - event.preventDefault(); - window.copySelection(); - return; - } - // CTRL/CMD+V - 粘贴 - if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { - event.preventDefault(); - window.pasteFromClipboard(); - return; - } - - // CTRL/CMD + SHIFT + DEL (Windows) �?CMD + SHIFT + Backspace (Mac) - 清空 - if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { - event.preventDefault(); - $("#clean").click(); - return; - } - - // CTRL/CMD + DEL (Windows) �?CMD + Backspace (Mac) - 删除 - if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { - event.preventDefault(); - $("#delete").click(); - return; - } - - } - // 兼容三端�?modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta�? - if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd�? - _keyctrl = true; - if (_keyc && _textEdit) { - //同时按下且在编辑状�? - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - } - } - if (event.keyCode == 67) { - _keyc = true; - if (_keyctrl & _textEdit) { - //同时按下且在编辑状�? - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - } - } - if (event.keyCode == 37) { - //left - let v = -1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5; - } - const currentCanvas = window.canvas || canvas; - if (currentCanvas && currentCanvas.getActiveObject()) { - let x = currentCanvas.getActiveObject().get('left'); - x += v; - currentCanvas.getActiveObject().set('left', x); - currentCanvas.renderAll(); - if (typeof window.recordState === 'function') { - window.recordState(); - } else if (typeof recordState === 'function') { - recordState(); - } - if (typeof updateControls === 'function') { - updateControls(); - } - } - } - if (event.keyCode == 38) { - //up - let v = -1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5; - } - const currentCanvas = window.canvas || canvas; - if (currentCanvas && currentCanvas.getActiveObject()) { - let y = currentCanvas.getActiveObject().get('top'); - y += v; - currentCanvas.getActiveObject().set('top', y); - currentCanvas.renderAll(); - if (typeof window.recordState === 'function') { - window.recordState(); - } else if (typeof recordState === 'function') { - recordState(); - } - if (typeof updateControls === 'function') { - updateControls(); - } - } - } - if (event.keyCode == 39) { - //right - let v = 1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5; - } - const currentCanvas = window.canvas || canvas; - if (currentCanvas && currentCanvas.getActiveObject()) { - let x = currentCanvas.getActiveObject().get('left'); - x += v; - currentCanvas.getActiveObject().set('left', x); - currentCanvas.renderAll(); - if (typeof window.recordState === 'function') { - window.recordState(); - } else if (typeof recordState === 'function') { - recordState(); - } - if (typeof updateControls === 'function') { - updateControls(); - } - } - } - if (event.keyCode == 40) { - //down - let v = 1; - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5; - } - const currentCanvas = window.canvas || canvas; - if (currentCanvas && currentCanvas.getActiveObject()) { - let y = currentCanvas.getActiveObject().get('top'); - y += v; - currentCanvas.getActiveObject().set('top', y); - currentCanvas.renderAll(); - if (typeof window.recordState === 'function') { - window.recordState(); - } else if (typeof recordState === 'function') { - recordState(); - } - if (typeof updateControls === 'function') { - updateControls(); - } - } - } - // 普通删除键(DEL键,没有按CTRL/CMD�? - if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { - // 使用 window.canvas 作为后备,确保在 eval() 加载 .jsc 时也能访问 - const currentCanvas = window.canvas || canvas; - const currentObjs = window.objs || objs; - if (!currentCanvas) { - return; - } - let actObjs = currentCanvas.getActiveObjects(); - let allObjs = currentCanvas.getObjects(); - let flag = 0; - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item, currentCanvas);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 - currentObjs.splice(i - 1, 1); - currentCanvas.remove(item); - flag = 1; - - } - } - if (typeof window.updateList === 'function') { - window.updateList(); - } - currentCanvas.discardActiveObject(); - if (flag == 1) { - if (typeof window.recordState === 'function') { - window.recordState(); - } else if (typeof recordState === 'function') { - recordState(); - } - currentCanvas.renderAll(); - if (typeof window.handleObjectSelected === 'function') { - window.handleObjectSelected(); - } - } - } -}); -// pre_add_image 已在 design1.js 中定义为全局变量 -$("#addPic").click(function () { - OpenDialog(); - function OpenDialog() { - var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); - if (!dialogApi) return; - dialogApi.showOpenDialog({ - title: language_str("selectPic"), - buttonLabel: language_str("comf"), - filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] - }).then(function(result) { - if (result.canceled) return; - var url = (result.files && result.files[0]) ? URL.createObjectURL(result.files[0]) : (result.filePaths && result.filePaths[0]); - if (!url) return; - fabric.Image.fromURL(url, function (image) { - //image.left = pointer.x; - image.setSrc(image.toDataURL(), function (image) { - pre_add_image = image; - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 1; - }); - //canvas.add(image); - }); - }).catch(err => { - }) - } -}); -$("#addOutPic").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 2; -}); -$("#addText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 3; -}); - -$("#addCircleText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 11; -}); -$("#addOutText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 4; -}); -$("#addRect").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 5; -}); -$("#addCircle").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 6; -}); -$("#addLine").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 7; -}); -$("#addQrCode").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 8; -}); -$("#addBarCode").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 9; -}); -$("#addCounter").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addCircleText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - addState = 10; -}); -$("#version_change").click(function () { - if (bg_version == 1) { - bg_version = 2; - } else { - bg_version = 1; - } - // 类型改变 - $("#source_front").attr("src", "./public/images/front_bg" + bg_version + "_1.png"); - canvas.renderAll(); - -}); -$("#front_side").click(function () { - $(this).addClass("ui-button-active"); - $("#back_side").removeClass("ui-button-active"); - $(".canvas-container:eq(0)").show(); - $(".canvas-container:eq(1)").hide(); - canvas = canvas1; - objs = objs1; - background_image = background_image1; - window.canvas = canvas; - window.objs = objs; - window.background_image = background_image; - next_objs = next_objs1; - pre_objs = pre_objs1; - next_json = next_json1; - pre_json = pre_json1; - recordObjs = recordObjs1; - recordJson = recordJson1; - step = step1; - if (typeof window.updateList === 'function') { - window.updateList(); - } - - $("#line_control").hide(); - $("#base_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//背景 -}); -$("#back_side").click(function () { - $(this).addClass("ui-button-active"); - $("#front_side").removeClass("ui-button-active"); - $(".canvas-container:eq(0)").hide(); - $(".canvas-container:eq(1)").show(); - canvas = canvas2; - objs = objs2; - background_image = background_image2; - window.canvas = canvas; - window.objs = objs; - window.background_image = background_image; - next_objs = next_objs2; - pre_objs = pre_objs2; - next_json = next_json2; - pre_json = pre_json2; - recordObjs = recordObjs2; - recordJson = recordJson2; - step = step2; - if (typeof window.updateList === 'function') { - window.updateList(); - } - $("#line_control").hide(); - $("#base_control").hide(); - $("#pic_control").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#circle_text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - $("#component_type").text(language_str("bg"));//"背景" -}); -$("#top_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一�? - let obj = _objs[0]; - obj.setCoords(); - let coords = obj.getCoords(true); - let objMinY = getCoordsMinY(coords); - let bgMinY = getCoordsMinY(background_image.getCoords(true)); - let deltaY = bgMinY - objMinY; - obj.set("top", obj.get("top") + deltaY); - obj.setCoords(); - canvas.renderAll(); - window.recordState(); - return; - } - // 多个对象对齐:找到所有对象边界框的最小Y值 - let minTop = Infinity; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinY = getCoordsMinY(coords); - if (itemMinY < minTop) { - minTop = itemMinY; - } - } - // 将所有对象对齐到最小Y值 - groupItems = []; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinY = getCoordsMinY(coords); - let deltaY = minTop - itemMinY; - item.set("top", item.get("top") + deltaY); - item.setCoords(); - groupItems.push(item); - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - -}); -$("#bottom_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一�? - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get("strokeWidth") - } - let obj = _objs[0]; - obj.setCoords(); - let coords = obj.getCoords(true); - let objMaxY = getCoordsMaxY(coords); - let bgMaxY = getCoordsMaxY(background_image.getCoords(true)); - let deltaY = bgMaxY - objMaxY; - obj.set("top", obj.get("top") + deltaY); - obj.setCoords(); - canvas.renderAll(); - window.recordState(); - return; - } - // 多个对象对齐:找到所有对象边界框的最大Y值 - let maxBottom = -Infinity; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMaxY = getCoordsMaxY(coords); - if (itemMaxY > maxBottom) { - maxBottom = itemMaxY; - } - } - // 将所有对象对齐到最大Y值 - groupItems = []; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMaxY = getCoordsMaxY(coords); - let deltaY = maxBottom - itemMaxY; - item.set("top", item.get("top") + deltaY); - item.setCoords(); - groupItems.push(item); - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - -}); -$("#center_y_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一�? - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get("strokeWidth") - } - let obj = _objs[0]; - obj.setCoords(); - let coords = obj.getCoords(true); - let objMinY = getCoordsMinY(coords); - let objMaxY = getCoordsMaxY(coords); - let objCenterY = (objMinY + objMaxY) / 2; - let bgCoords = background_image.getCoords(true); - let bgMinY = getCoordsMinY(bgCoords); - let bgMaxY = getCoordsMaxY(bgCoords); - let bgCenterY = (bgMinY + bgMaxY) / 2; - let deltaY = bgCenterY - objCenterY; - obj.set("top", obj.get("top") + deltaY); - obj.setCoords(); - canvas.renderAll(); - window.recordState(); - return; - } - // 多个对象对齐:找到所有对象边界框的中心Y值 - let minTop = Infinity; - let maxBottom = -Infinity; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinY = getCoordsMinY(coords); - let itemMaxY = getCoordsMaxY(coords); - if (itemMinY < minTop) minTop = itemMinY; - if (itemMaxY > maxBottom) maxBottom = itemMaxY; - } - let groupCenterY = (minTop + maxBottom) / 2; - // 将所有对象对齐到中心Y值 - groupItems = []; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinY = getCoordsMinY(coords); - let itemMaxY = getCoordsMaxY(coords); - let itemCenterY = (itemMinY + itemMaxY) / 2; - let deltaY = groupCenterY - itemCenterY; - item.set("top", item.get("top") + deltaY); - item.setCoords(); - groupItems.push(item); - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - window.recordState(); -}); -$("#left_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一�? - let obj = _objs[0]; - obj.setCoords(); - let coords = obj.getCoords(true); - let objMinX = getCoordsMinX(coords); - let bgMinX = getCoordsMinX(background_image.getCoords(true)); - let deltaX = bgMinX - objMinX; - obj.set("left", obj.get("left") + deltaX); - obj.setCoords(); - canvas.renderAll(); - window.recordState(); - return; - } - // 多个对象对齐:找到所有对象边界框的最小X值 - let minLeft = Infinity; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinX = getCoordsMinX(coords); - if (itemMinX < minLeft) { - minLeft = itemMinX; - } - } - // 将所有对象对齐到最小X值 - groupItems = []; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinX = getCoordsMinX(coords); - let deltaX = minLeft - itemMinX; - item.set("left", item.get("left") + deltaX); - item.setCoords(); - groupItems.push(item); - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - window.recordState(); -}); -$("#right_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一�? - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get("strokeWidth") - } - let obj = _objs[0]; - obj.setCoords(); - let coords = obj.getCoords(true); - let objMaxX = getCoordsMaxX(coords); - let bgMaxX = getCoordsMaxX(background_image.getCoords(true)); - let deltaX = bgMaxX - objMaxX; - obj.set("left", obj.get("left") + deltaX); - obj.setCoords(); - canvas.renderAll(); - window.recordState(); - return; - } - // 多个对象对齐:找到所有对象边界框的最大X值 - let maxRight = -Infinity; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMaxX = getCoordsMaxX(coords); - if (itemMaxX > maxRight) { - maxRight = itemMaxX; - } - } - // 将所有对象对齐到最大X值 - let groupItems = []; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMaxX = getCoordsMaxX(coords); - let deltaX = maxRight - itemMaxX; - item.set("left", item.get("left") + deltaX); - item.setCoords(); - groupItems.push(item); - } - //BUG1 - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - window.recordState(); -}); -$("#center_x_alignment").click(function () { - let _objs = canvas.getActiveObjects(); - if (_objs.length == 1) { - //只选中了一�? - let idx = getIndex(_objs[0]); - let type = objs[idx - 1].type; - let other = 0;//strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get("strokeWidth") - } - let obj = _objs[0]; - obj.setCoords(); - let coords = obj.getCoords(true); - let objMinX = getCoordsMinX(coords); - let objMaxX = getCoordsMaxX(coords); - let objCenterX = (objMinX + objMaxX) / 2; - let bgCoords = background_image.getCoords(true); - let bgMinX = getCoordsMinX(bgCoords); - let bgMaxX = getCoordsMaxX(bgCoords); - let bgCenterX = (bgMinX + bgMaxX) / 2; - let deltaX = bgCenterX - objCenterX; - obj.set("left", obj.get("left") + deltaX); - obj.setCoords(); - canvas.renderAll(); - window.recordState(); - return; - } - // 多个对象对齐:找到所有对象边界框的中心X值 - let minLeft = Infinity; - let maxRight = -Infinity; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinX = getCoordsMinX(coords); - let itemMaxX = getCoordsMaxX(coords); - if (itemMinX < minLeft) minLeft = itemMinX; - if (itemMaxX > maxRight) maxRight = itemMaxX; - } - let groupCenterX = (minLeft + maxRight) / 2; - // 将所有对象对齐到中心X值 - groupItems = []; - for (let item of _objs) { - item.setCoords(); - let coords = item.getCoords(true); - let itemMinX = getCoordsMinX(coords); - let itemMaxX = getCoordsMaxX(coords); - let itemCenterX = (itemMinX + itemMaxX) / 2; - let deltaX = groupCenterX - itemCenterX; - item.set("left", item.get("left") + deltaX); - item.setCoords(); - groupItems.push(item); - } - canvas.discardActiveObject(); - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas, - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - canvas.renderAll(); - window.recordState(); -}); -$("#up_obj").click(function () {//上移,放到前�?数组中在后面 - let idx = getIndex(canvas.getActiveObject()); - if (idx + 1 != canvas.getObjects().length) { - canvas.bringForward(canvas.getActiveObject()); - canvas.renderAll(); - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx]; - objs[idx] = temp; - //console.log(objs); - - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(idx); - } - } -}); -$("#down_obj").click(function () {//下移,放到后�?数组中在前面 - let idx = getIndex(canvas.getActiveObject()); - if (idx != 1) { - canvas.sendBackwards(canvas.getActiveObject()); - canvas.renderAll(); - //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; - - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx - 2]; - objs[idx - 2] = temp; - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(idx - 2); - } - } -}); -$("#set_bg").click(function () {//设置为背�? - let obj = canvas.getActiveObject(); - let idx = getIndex(obj); - const scaleX = 2787 / obj.width - const scaleY = 2787 / obj.height - // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 - const scaledWidth = obj.width * scaleX; - const scaledHeight = obj.height * scaleY; - obj.set({ - scaleX: scaleX, - scaleY: scaleY, - left: ($('#canvas1').width() - scaledWidth) / 2, - top: ($('#canvas1').height() - scaledHeight) / 2, - }); - $('#text_w').val(2787) - $('#text_h').val(2787) - $('#text_x').val(0) - $('#text_y').val(0) - if (obj.get("selectable")) { - obj.set("selectable", false); - $("#lock_img").attr("src", "public/images/lock20.png") - } - canvas.sendToBack(obj); - canvas.bringForward(obj); - canvas.renderAll(); - let objItem = objs.splice(idx - 1, 1)[0] - objs.unshift(objItem) - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(0); - } -}); - -// ========================================== -// onStateRestored 函数在 core.js 中定义并附加到 window 对象 -// ========================================== -$("#previous").click(function () { - // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 - if (window._isApplyingHistory) { - return; - } - - if (step.val == 0) return; - - step.val--; - - // 判断当前是正面还是背面 - if ($("#front_side").hasClass("ui-button-active")) { - // 1. 恢复业务数据 - objs1 = JSON.parse(recordObjs1[step.val]); - objs = objs1; - - // 2. 恢复画布 (使用回调函数) - window._isApplyingHistory = true; - canvas.loadFromJSON(recordJson1[step.val], function () { - // 加载后解锁所有对象(背景除外) - if (typeof window.unlockAllObjects === 'function') { - window.unlockAllObjects(canvas); - } - // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) - if (typeof window.regenerateQrCodesAndBarcodes === 'function') { - window.regenerateQrCodesAndBarcodes(canvas, objs1); - } - if (typeof window.onStateRestored === 'function') { - window.onStateRestored(); // 图片加载完才执行这一行 - } - window._isApplyingHistory = false; - }); - } else { - // 背面逻辑 - objs2 = JSON.parse(recordObjs2[step.val]); - objs = objs2; - - window._isApplyingHistory = true; - canvas.loadFromJSON(recordJson2[step.val], function () { - // 加载后解锁所有对象(背景除外) - if (typeof window.unlockAllObjects === 'function') { - window.unlockAllObjects(canvas); - } - // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) - if (typeof window.regenerateQrCodesAndBarcodes === 'function') { - window.regenerateQrCodesAndBarcodes(canvas, objs2); - } - if (typeof window.onStateRestored === 'function') { - window.onStateRestored(); - } - window._isApplyingHistory = false; - }); - } -}); -// ========================================== -$("#next").click(function () { - // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 - if (window._isApplyingHistory) { - return; - } - - // 获取当前历史记录的最大长度 - let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; - - if (step.val >= maxLen - 1) return; - - step.val++; - - if ($("#front_side").hasClass("ui-button-active")) { - objs1 = JSON.parse(recordObjs1[step.val]); - objs = objs1; - - window._isApplyingHistory = true; - canvas.loadFromJSON(recordJson1[step.val], function () { - // 加载后解锁所有对象(背景除外) - if (typeof window.unlockAllObjects === 'function') { - window.unlockAllObjects(canvas); - } - // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) - if (typeof window.regenerateQrCodesAndBarcodes === 'function') { - window.regenerateQrCodesAndBarcodes(canvas, objs1); - } - if (typeof window.onStateRestored === 'function') { - window.onStateRestored(); // 图片加载完才执行这一行 - } - window._isApplyingHistory = false; - }); - } else { - objs2 = JSON.parse(recordObjs2[step.val]); - objs = objs2; - - window._isApplyingHistory = true; - canvas.loadFromJSON(recordJson2[step.val], function () { - // 加载后解锁所有对象(背景除外) - if (typeof window.unlockAllObjects === 'function') { - window.unlockAllObjects(canvas); - } - // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) - if (typeof window.regenerateQrCodesAndBarcodes === 'function') { - window.regenerateQrCodesAndBarcodes(canvas, objs2); - } - if (typeof window.onStateRestored === 'function') { - window.onStateRestored(); - } - window._isApplyingHistory = false; - }); - } -}); - -$("#clean").click(function () { - objs.splice(0, objs.length); - - let allObjs = canvas.getObjects(); - for (let item of allObjs) { - if (item != allObjs[0]) { - canvas.remove(item); - } - } - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}); -$("#rotate").click(function () { - let obj = canvas.getActiveObject(); - obj.rotate(Math.round((obj.get("angle") + 90) % 360)); - $("#text_r").val(obj.get("angle")); - canvas.renderAll(); - updateControls(); - window.recordState(); -}); -$('#vertical_obj').click(function () { - let obj = canvas.getActiveObject(); - obj.set("flipY", !obj.get("flipY")); - canvas.renderAll(); - updateControls(); - window.recordState(); -}); -$('#align_obj').click(function () { - let obj = canvas.getActiveObject(); - obj.set("flipX", !obj.get("flipX")); - canvas.renderAll(); - updateControls(); - window.recordState(); -}); -$("#text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�?恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#text_underline").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("underline", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("underline", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#text_linethrough").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("linethrough", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("linethrough", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); - -$("#circle_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�?恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); - -$('#out_left_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_left_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'left') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'left' - window.recordState(); - canvas.renderAll(); -}) -$('#out_right_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_right_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'right') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'right' - window.recordState(); - canvas.renderAll(); -}) -$('#out_center_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_center_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'center') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'center' - window.recordState(); - canvas.renderAll(); -}) - -$("#out_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�?恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_underline").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("underline", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("underline", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_linethrough").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已�? - canvas.getActiveObject().set("linethrough", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未�? - canvas.getActiveObject().set("linethrough", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#lock").off('click').on('click', function (e) { - e.stopPropagation(); - if (!canvas.getActiveObject()) return; - let obj = canvas.getActiveObject(); - // 检查是否已锁定(通过lockMovementX判断,因为锁定时会设置所有lock属性) - let isLocked = obj.get("lockMovementX") === true; - let shouldLock = !isLocked; - - // 锁定/解锁所有编辑操作,但保持选中状态(不取消选中) - obj.set({ - lockMovementX: shouldLock, - lockMovementY: shouldLock, - lockRotation: shouldLock, - lockScalingX: shouldLock, - lockScalingY: shouldLock, - lockSkewingX: shouldLock, - lockSkewingY: shouldLock, - // 锁定时不可选中,解锁时可选中(但保持当前选中状态) - selectable: !shouldLock, - evented: !shouldLock - }); - if (shouldLock) { - $("#lock_img").attr("src", "public/images/lock20.png")//已锁定 - } else { - $("#lock_img").attr("src", "public/images/unlock20.png")//已解锁 - } - updateControls(); - window.recordState(); - // 更新对象列表以同步锁状态 - if (typeof window.updateList === 'function') { - window.updateList(); - } - canvas.renderAll(); -}); -$("#text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#text_back_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#circle_text_back_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#out_text_back_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#rect_transparent").click(function () { - canvas.getActiveObject().set("fill", ""); - $("#rect_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#rect_stroke_transparent").click(function () { - canvas.getActiveObject().set("stroke", ""); - $("#rect_stroke").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_transparent").click(function () { - canvas.getActiveObject().set("fill", ""); - $("#circle_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_stroke_transparent").click(function () { - canvas.getActiveObject().set("stroke", ""); - $("#circle_stroke").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); - -// saveAs 函数保留,因为它被作为回调传递给 output 函数(第52行) -function saveAs(op1, callback) { - const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; - let j = canvas1.toJSON(props); - let b = canvas2.toJSON(props); - - // 清理不需要的 src 字段(使用 window.cleanupSrcFields,在 output.js 中定义) - if (typeof window.cleanupSrcFields === 'function') { - window.cleanupSrcFields(j.objects, objs1); - window.cleanupSrcFields(b.objects, objs2); - } - - let o = { f: j, b: {}, fo: objs1, bo: objs2 }; - let con_o = Object.assign(o, op1); - var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); - if (!dialogApi) return; - dialogApi.showSaveDialog({ - title: language_str("saveFile"), - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }).then(function(result) { - var fp = result.filePath || (result.fileHandle && result.fileHandle.name); - if (!fp && !result.fileHandle) return; - if (typeof fp === 'string' && fp.substring(fp.length - 5).indexOf('.') == -1) fp += '.soon'; - con_o.soonType = 1; - var content = JSON.stringify(con_o); - if (window.platformBridge && window.platformBridge.writeFile) { - window.platformBridge.writeFile(result.fileHandle || fp, content).then(function() { - openAs.name = fp; - layer.msg(language_str("saveSucc") + openAs.name); - if (typeof window.saveHistory === 'function') window.saveHistory(); - if (callback && typeof callback === 'function') callback(); - }).catch(function() {}); - return; - } - var fs = typeof require !== 'undefined' ? require('fs') : null; - if (fs) { - fs.writeFileSync(result.filePath || fp, content, 'utf8'); - openAs.name = result.filePath || fp; - layer.msg(language_str("saveSucc") + openAs.name); - if (typeof window.saveHistory === 'function') window.saveHistory(); - if (callback && typeof callback === 'function') callback(); - } - }).catch(function(err) {}); -} -// save 函数已在 output.js 中定义 -$("#copy").click(function () { - window.copySelection(); -}) -// 粘贴 -$("#paste").click(function () { - window.pasteFromClipboard(); -}) - -// 保存 (保持你原有的保存逻辑不变) -$("#save").click(function () { - if (typeof window.output === 'function') { - window.output(); // 这里调用你的 output 函数 - } - return; - // ... (下面是你原有的被 return 截断的代码,建议以后清理掉,但这里我按要求保�? - let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); - // ... (省略你原来那段很长的保存代码) ... -}); - -function getDate() { - let date = new Date(); - return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); -} -function getAbsoluteXY(_obj) { - let coord = _obj.get("lineCoords"); - return [_obj.get("left"), _obj.get("top")] -} -// fullPath 和 saveHistory 已在 output.js 中定义 -$("#open").click(function () { - if (step.val == 0) { - OpenDialog(); - return; - } - layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件�? - if (typeof window.output === 'function') { - window.output(OpenDialog); - } - layer.close(index); - - }, function (index) { - OpenDialog(); - }); - function OpenDialog() { - var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); - if (!dialogApi) return; - dialogApi.showOpenDialog({ - title: "请选择文件", - buttonLabel: language_str("comf"), - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }).then(function(result) { - if (result.canceled) return; - var filePath = result.filePaths && result.filePaths[0]; - var file = (result.files && result.files[0]) || result.file; - function onData(j) { - var nameOrPath = (file && file.name) ? file.name : filePath; - var type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; - if (type == 2) { - if (ipcRenderer) ipcRenderer.send('open-design-page', nameOrPath, type); - else if (window.platformBridge) window.platformBridge.openDesignPage(nameOrPath, type); - return; - } - if (typeof window.openFile === 'function') { - window.openFile(nameOrPath, j); - } - } - if (file && file.text) { - file.text().then(function(t) { onData(JSON.parse(t)); }).catch(function() {}); - return; - } - if (!filePath) return; - var fs = typeof require !== 'undefined' ? require('fs') : null; - if (fs) { - var fsData = fs.readFileSync(filePath); - onData(JSON.parse(fsData.toString())); - } - }).catch(function(err) {}); - } -}); -//let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; -let t_objs1 = canvas1.getObjects(); -let t_objs2 = canvas2.getObjects(); -for (let i = 1; i < t_objs1.length; i++) { - canvas1.remove(t_objs1[i]); -} -for (let i = 1; i < t_objs2.length; i++) { - canvas2.remove(t_objs2[i]); -} -objs2 = []; -objs1 = []; -step1.val = 0; -step2.val = 0; -recordJson1 = []; -recordJson2 = []; -recordObjs1 = []; -recordObjs2 = []; -if ($("#front_side").hasClass("ui-button-active")) { - objs = objs1; - recordObjs = recordObjs1; - recordJson = recordJson1; - step = step1; -} else if ($("#back_side").hasClass("ui-button-active")) { - objs = objs2; - recordObjs = recordObjs2; - recordJson = recordJson2; - step = step2; -} -openAs.name = ""; -canvas1.renderAll(); -canvas2.renderAll(); -if (typeof window.updateList === 'function') { - window.updateList(); -} -$("#new").click(function () { - if (step.val == 0) { - if (ipcRenderer) ipcRenderer.send('open-first-page'); - else if (window.platformBridge) window.platformBridge.openFirstPage(); - return; - } - layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件�? - if (typeof window.output === 'function') { - window.output(function() { if (ipcRenderer) ipcRenderer.send('open-first-page'); else if (window.platformBridge) window.platformBridge.openFirstPage(); }); - } - }, function (index) { - if (ipcRenderer) ipcRenderer.send('open-first-page'); - else if (window.platformBridge) window.platformBridge.openFirstPage(); - }); -}); - -// =========================================================== -$("#text_y").bind('input propertychange', function () { - if (!canvas.getActiveObject()) return; - if ($("#text_y").val() == "") { - $("#text_y").val(0) - } - let val = background_image.top; - if ($("#text_y").val() != "") { - val = background_image.top + parseInt($("#text_y").val()); - } - canvas.getActiveObject().set("top", val) - canvas.renderAll(); - window.recordState(); -}) -$("#text_x").bind('input propertychange', function () { - if (!canvas.getActiveObject()) return; - if ($("#text_x").val() == "") { - $("#text_x").val(0) - } - let val = background_image.left; - if ($("#text_x").val() != "") { - val = background_image.left + parseInt($("#text_x").val()); - } - canvas.getActiveObject().set("left", val) - canvas.renderAll(); - window.recordState(); -}) -$("#text_w").bind('input propertychange', function () { - if (!canvas.getActiveObject()) return; - let val = "0"; - if ($("#text_w").val() != "") { - val = $("#text_w").val(); - } - let idx = getIndex(canvas.getActiveObject()); - if (idx <= 0 || !objs[idx - 1]) return; - let type = objs[idx - 1].type; - if (type == 3 || type == 4) { - canvas.getActiveObject().set("width", parseInt(val)) - } else { - canvas.getActiveObject().set("scaleX", parseInt(val) / canvas.getActiveObject().get("width")) - } - - canvas.renderAll(); - window.recordState(); -}) -$("#text_h").bind('input propertychange', function () { - if (!canvas.getActiveObject()) return; - let idx = getIndex(canvas.getActiveObject()); - if (idx <= 0 || !objs[idx - 1]) return; - if (objs[idx - 1].type == 7) { - if ($("#text_h").val() == "") { - $("#text_h").val(canvas.getActiveObject().get("strokeWidth")); - } - canvas.getActiveObject().set("strokeWidth", parseInt($("#text_h").val())); - canvas.renderAll(); - return; - } - let val = "0"; - if ($("#text_h").val() != "") { - val = $("#text_h").val(); - } - let type = objs[idx - 1].type; - if (type == 3 || type == 4) { - canvas.getActiveObject().set("height", parseInt(val)) - } else { - canvas.getActiveObject().set("scaleY", parseInt(val) / canvas.getActiveObject().get("height")) - } - - canvas.renderAll(); - window.recordState(); -}) -$("#text_r").bind('input propertychange', function () { - if (!canvas.getActiveObject()) return; - if ($("#text_r").val() == "") { - $("#text_r").val("0") - } - $("#text_r").val(parseInt($("#text_r").val()) % 360) - //canvas.getActiveObject().set("rotate", val) - canvas.getActiveObject().rotate(parseInt($("#text_r").val())) - canvas.renderAll(); - window.recordState(); -}) -$("#out_pic_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#out_pic_field_name").val(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}) - -$("#text_black").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].black = true; - } else { - objs[idx - 1].black = false; - } - window.recordState(); -}); -$("#out_text_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#out_text_field_name").val(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}) -$("#qrcode_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#qrcode_field_name").val(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}) -$("#qrcode_color").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let qr = jrQrcode.getQrBase64($("#qrcode_val").val(), { padding: 0, foreground: $(this).val() }); - let idx = getIndex(item); - objs[idx - 1].color = $(this).val(); - item.setSrc(qr, function (img) { - canvas.renderAll(); - window.recordState(); - }); -}); -$("#qrcode_val").bind('input propertychange', function () { - let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' - let item = canvas.getActiveObject(); - let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); - let idx = getIndex(item); - objs[idx - 1].val = val; - item.setSrc(qr, function (img) { - canvas.renderAll(); - window.recordState(); - }); -}); -$("#barcode_color").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].color = $(this).val(); - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - window.recordState(); - }); -}); -$("#barcode_field_name").bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()); - objs[idx - 1].name = $("#barcode_field_name").val(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}) -$("#text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#text_color").val()); - canvas.renderAll(); - window.recordState(); -}) -$("#text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#text_back_color").val()); - canvas.renderAll(); - window.recordState(); -}) -$("#text_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#text_size").val())) { - val = parseFloat($("#text_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val % 0.5 !== 0) { - val = Math.round(val * 2) / 2 - } - - } - $("#text_size").val(val) - canvas.getActiveObject().set("fontSize", val * dpi / 72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - window.recordState(); -}) -$("#circle_text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#circle_text_color").val()); - canvas.renderAll(); - window.recordState(); -}) -$("#circle_text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); - - canvas.renderAll(); - window.recordState(); -}) -$("#circle_text_text").bind('input propertychange', function () { - let val = $("#circle_text_text").val().trim() - // 有内容时不加首行缩进(前面的空格),只保留后面的空格 - canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); - canvas.renderAll(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}) -$("#out_text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#out_text_color").val()); - canvas.renderAll(); - window.recordState(); -}) -$("#out_text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#out_text_back_color").val()); - - canvas.renderAll(); - window.recordState(); -}) -$("#out_text_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#out_text_size").val())) { - val = parseFloat($("#out_text_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val % 0.5 !== 0) { - val = Math.round(val * 2) / 2 - } - - } - $("#out_text_size").val(val) - canvas.getActiveObject().set("fontSize", val * dpi / 72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - window.recordState(); -}) -// 监听合成文本输入框 (占位符) -$("#out_text_text").on('input propertychange', function () { - let val = $(this).val(); - - // 【核心修改】如果值为空,强制给 2 个空格 - if (val === "") { - val = " "; - } - - let obj = canvas.getActiveObject(); - if (obj) { - obj.set("text", val); - obj.setCoords(); - canvas.requestRenderAll(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); - } -}); -$("#rect_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#rect_color").val()); - - canvas.renderAll(); - window.recordState(); -}) -$("#rect_stroke").bind('input propertychange', function () { - canvas.getActiveObject().set("stroke", $("#rect_stroke").val()); - - canvas.renderAll(); - window.recordState(); -}) -$("#rect_stroke_width").bind('input propertychange', function () { - if ($("#rect_stroke_width").val() == "") { - $("#rect_stroke_width").val("0") - } else { - $("#rect_stroke_width").val(parseInt($("#rect_stroke_width").val())) - } - canvas.getActiveObject().set("strokeWidth", parseInt($("#rect_stroke_width").val())); - canvas.renderAll(); - window.recordState(); -}) -$("#rect_radius").bind('input propertychange', function () { - if ($("#rect_radius").val() == "") { - $("#rect_radius").val("0") - } else { - $("#rect_radius").val(parseInt($("#rect_radius").val())) - } - canvas.getActiveObject().set("rx", parseInt($("#rect_radius").val())); - canvas.getActiveObject().set("ry", parseInt($("#rect_radius").val())); - canvas.renderAll(); - window.recordState(); -}) -$("#circle_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#circle_color").val()); - - canvas.renderAll(); - window.recordState(); -}) -$("#circle_stroke").bind('input propertychange', function () { - canvas.getActiveObject().set("stroke", $("#circle_stroke").val()); - - canvas.renderAll(); - window.recordState(); -}) -$("#circle_stroke_width").bind('input propertychange', function () { - if ($("#circle_stroke_width").val() == "") { - $("#circle_stroke_width").val("0") - } else { - $(this).val(parseInt($(this).val())) - } - canvas.getActiveObject().set("strokeWidth", parseInt($("#circle_stroke_width").val())); - canvas.renderAll(); - window.recordState(); -}) -$("#counter_start").bind('input propertychange', function () { - canvas.getActiveObject().set("text", $("#counter_start").val()); - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].start = $(this).val(); - canvas.renderAll(); - window.recordState(); -}) -$("#counter_add").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].add = $(this).val(); - canvas.renderAll(); - window.recordState(); -}) -$("#counter_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#counter_color").val()); - canvas.renderAll(); - window.recordState(); -}) -$("#count_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#count_size").val())) { - val = parseFloat($("#count_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val % 0.5 !== 0) { - val = Math.round(val * 2) / 2 - } - - } - $("#count_size").val(val) - canvas.getActiveObject().set("fontSize", val * dpi / 72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - window.recordState(); -}) -$("#line_color").bind('input propertychange', function () { - canvas.getActiveObject().set("stroke", $("#line_color").val()); - canvas.renderAll(); - window.recordState(); -}) -$("#line_dist").bind('input propertychange', function () { - if ($("#line_dist").val() == "") { - $("#line_dist").val(0) - } - canvas.getActiveObject().set("strokeDashArray", [parseInt($("#line_dist").val()), parseInt($("#line_dist").val())]) - canvas.renderAll(); - window.recordState(); -}) -// 监听普通文本输入 (占位符) -$("#text_text").on('input propertychange', function () { - // 1. 获取输入框的值,不要用 trim(),否则无法输入空格 - let val = $(this).val(); - - // 2. 核心修改:只有当删光时,才给 4 个空格作为占位符 - if (val === "") { - val = " "; // 4个空格,比原来的2个更宽,更好点 - } - // 3. 更新对象 - canvas.getActiveObject().set("text", val); - canvas.requestRenderAll(); // 使用 requestRenderAll 性能更好 - if (typeof window.updateList === 'function') { - window.updateList(); - } - window.recordState(); -}); -$("#barcode_val").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].val = $(this).val(); - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - window.recordState(); - }); -}) -$("#barcode_show").click(function () { - let item = canvas.getActiveObject(); - let idx = getIndex(canvas.getActiveObject()); - let val = $("#barcode_size").val(); - let displayValue = true; - if ($(this).prop("checked")) { - objs[idx - 1].show = true; - displayValue = false; - } else { - objs[idx - 1].show = false; - displayValue = true; - } - //val = 0; // Remove this hack - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val * dpi / 72, font: objs[idx - 1].font, displayValue: displayValue }); - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - window.recordState(); - }); -}); -$("#barcode_size").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - let val = 1 - if (parseFloat($(this).val())) { - val = parseFloat($(this).val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val % 0.5 !== 0) { - val = Math.round(val * 2) / 2 - } - - } - $("#barcode_size").val(val) - objs[idx - 1].fontSize = val * dpi / 72; - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - window.recordState(); - }); -}) -$('#count_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - window.recordState(); -}); -$('#text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - window.recordState(); -}); - -$('#circle_text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - window.recordState(); -}); - -$('#language_select').on('change', function () {//语言选择 - langua_ge($(this).find('option:selected').val()); - s_lan = $(this).find('option:selected').val(); - localStorage.setItem("lang", s_lan); -}); -$('#barcode_font_family').on('change', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - objs[idx - 1].font = $(this).find('option:selected').text(); - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - window.recordState(); - }); - //window.recordState(); -}); -$('#out_text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - window.recordState(); -}); -$(".obj_list").on("click", "div", function () { - let idx = objs.length - 1 - $(this).index(); - if (typeof window.selectObj === 'function') { - window.selectObj(idx); - } -}); -$("#exceed").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].exceed = true; - } else { - objs[idx - 1].exceed = false; - } - window.recordState(); -}); -$("#autoWrap").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - - objs[idx - 1].autoWrap = true; - } else { - objs[idx - 1].autoWrap = false; - } - window.recordState(); -}); -$("#fixSize").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - - objs[idx - 1].fixSize = true; - } else { - objs[idx - 1].fixSize = false; - } - window.recordState(); -}); -$("#circle_text_center").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].circleCenter = 1; - } else { - objs[idx - 1].circleCenter = 0; - } - window.recordState(); -}); - -// =========================================================== -$("#text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#text_underline").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("underline", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("underline", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#text_linethrough").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("linethrough", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("linethrough", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); - -$("#circle_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); - -$('#out_left_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_left_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'left') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'left' - window.recordState(); - canvas.renderAll() -}) -$('#out_right_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_right_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'right') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'right' - window.recordState(); - canvas.renderAll() -}) -$('#out_center_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_center_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'center') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'center' - window.recordState(); - canvas.renderAll() -}) - -$("#out_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_underline").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("underline", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("underline", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_linethrough").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("linethrough", false) - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("linethrough", true) - $(this).addClass("ui-radio-acitve") - } - window.recordState(); - canvas.renderAll(); - window.recordState(); -}); - -// =========================================================== -$("#delete").click(function () { - let actObjs = canvas.getActiveObjects(); - let allObjs = canvas.getObjects(); - let flag = 0; - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 - objs.splice(i - 1, 1); - canvas.remove(item); - flag = 1; - - } - } - if (typeof window.updateList === 'function') { - window.updateList(); - } - canvas.discardActiveObject(); - if (flag == 1) { - window.recordState(); - canvas.renderAll(); - } -}); -// 移至上层和移至下层:先取消所有绑定(包括第一处的绑定),然后重新绑定(确保只绑定一次) -// 注意:第一处的事件绑定(第 842 行和第 861 行)会被这里的 off('click') 取消 -$("#up_obj").off('click').on('click', function () {//上移,放到前面 数组中在后面 - let idx = getIndex(canvas.getActiveObject()); - if (idx + 1 != canvas.getObjects().length) { - canvas.bringForward(canvas.getActiveObject()); - canvas.renderAll(); - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx]; - objs[idx] = temp; - - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(idx); - } - } -}); -$("#down_obj").off('click').on('click', function () {//下移,放到后面 数组中在前面 - let idx = getIndex(canvas.getActiveObject()); - if (idx != 1) { - canvas.sendBackwards(canvas.getActiveObject()); - canvas.renderAll(); - - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx - 2]; - objs[idx - 2] = temp; - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(idx - 2); - } - } -}); -$("#set_bg").click(function () {//设置为背景 - let obj = canvas.getActiveObject(); - let idx = getIndex(obj); - const scaleX = 2787 / obj.width - const scaleY = 2787 / obj.height - // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 - const scaledWidth = obj.width * scaleX; - const scaledHeight = obj.height * scaleY; - obj.set({ - scaleX: scaleX, - scaleY: scaleY, - left: ($('#canvas1').width() - scaledWidth) / 2, - top: ($('#canvas1').height() - scaledHeight) / 2, - }); - $('#text_w').val(2787) - $('#text_h').val(2787) - $('#text_x').val(0) - $('#text_y').val(0) - // 设为背景时,锁定所有编辑操作,并设置为不可选中 - obj.set({ - lockMovementX: true, - lockMovementY: true, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - lockSkewingX: true, - lockSkewingY: true, - selectable: false, - evented: false - }); - $("#lock_img").attr("src", "public/images/lock20.png") - canvas.sendToBack(obj); - canvas.bringForward(obj); - canvas.renderAll(); - let objItem = objs.splice(idx - 1, 1)[0] - objs.unshift(objItem) - - // 确保背景图(索引0)的锁定属性被正确设置 - setTimeout(() => { - let bgObj = canvas.getObjects()[0]; - if (bgObj) { - bgObj.set({ - lockMovementX: true, - lockMovementY: true, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - lockSkewingX: true, - lockSkewingY: true, - selectable: false, - evented: false - }); - canvas.renderAll(); - } - }, 100); - - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(0); - } -}); -// 重复的lock点击事件已移除,使用第一处的绑定 -$("#text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#text_back_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#circle_text_back_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#out_text_back_transparent").click(function () { - canvas.getActiveObject().set("textBackgroundColor", ""); - $("#out_text_back_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#rect_transparent").click(function () { - canvas.getActiveObject().set("fill", ""); - $("#rect_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#rect_stroke_transparent").click(function () { - canvas.getActiveObject().set("stroke", ""); - $("#rect_stroke").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_transparent").click(function () { - canvas.getActiveObject().set("fill", ""); - $("#circle_color").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); -$("#circle_stroke_transparent").click(function () { - canvas.getActiveObject().set("stroke", ""); - $("#circle_stroke").val("#ffffff"); - canvas.renderAll(); - window.recordState(); -}); - -// =========================================================== -$("#about").click(async function () { - // 1. 先异步获取版本号 - let version = "0.0.0"; // 默认值,防止获取失败 - try { - if (window.sysAPI && window.sysAPI.getAppVersion) { - const v = await window.sysAPI.getAppVersion(); - version = "v" + v; // 拼接一个 v,变成 v2.3.017 - } - } catch (e) { - } - - // 2. 获取成功后再弹出窗口 - layer.open({ - type: 1 - , title: language_str("about") - , area: '450px;' - , id: 'LAY_layuipro' - , moveType: 1 - , content: '
' + - '
' + - '
Soon Design
' + - // 【修改点】这里使用了刚才获取到的 version 变量 - '
' + language_str("vers") + ' ' + version + '
' + - '
' + language_str("copyright") + '
' + - '
' - }); -}) -$("#help").click(function() { - if (window.platformBridge && window.platformBridge.openHelp) window.platformBridge.openHelp(); - else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-help-file'); -}); - -// =========================================================== -$('#display').on('click', function () { - // 类型改变 - fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_1.png', function (i1) { - i1.left = background_image.left; - i1.top = background_image.top; - let img1 = i1; - - fabric.Image.fromURL('./public/images/back_bg_1.png', function (i2) { - i2.left = background_image.left; - i2.top = background_image.top; - let img2 = i2; - fabric.Image.fromURL('./public/images/op_1.png', function (i3) { - i3.left = background_image.left; - i3.top = background_image.top; - let img3 = i3; - if (typeof window.display_func === 'function') { - window.display_func(img1, img2, img3); - } - }) - }) - }) -}); -$("#output").click(function () { - // 另存为:传递 saveAs 函数作为第二个参数 - if (typeof window.output === 'function') { - window.output(null, saveAs); // 调用output函数,使用saveAs进行另存为 - } -}); - -// 确保移至上层和移至下层的事件绑定正确(在文件末尾再次确认) -// 取消所有绑定,然后重新绑定第二处的实现(第 2171 行和第 2189 行) -$(document).ready(function () { - setTimeout(function () { - $("#up_obj").off('click').on('click', function () { - let idx = getIndex(canvas.getActiveObject()); - if (idx + 1 != canvas.getObjects().length) { - canvas.bringForward(canvas.getActiveObject()); - canvas.renderAll(); - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx]; - objs[idx] = temp; - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(idx); - } - } - }); - $("#down_obj").off('click').on('click', function () { - let idx = getIndex(canvas.getActiveObject()); - if (idx != 1) { - canvas.sendBackwards(canvas.getActiveObject()); - canvas.renderAll(); - let temp = objs[idx - 1]; - objs[idx - 1] = objs[idx - 2]; - objs[idx - 2] = temp; - window.recordState(); - if (typeof window.updateList === 'function') { - window.updateList(); - } - if (typeof window.selectObj === 'function') { - window.selectObj(idx - 2); - } - } - }); - }, 100); -}); +// 移至上层和移至下层的事件处理在下方(第 2179 行),避免重复绑定 + +var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false; +$('input').focus(function () { + _inputEdit = true; +}); +$('input').blur(function () { + _inputEdit = false; +}); +$('textarea').focus(function () { + _inputEdit = true; +}); +$('textarea').blur(function () { + _inputEdit = false; +}); +$("body").keyup(function (event) { + // 兼容三端�?modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta�? + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释�? + _keyctrl = false; + }; + if (event.keyCode == 67) { + _keyc = false; + }; +}) +$("body").keydown(function (event) { + // 统一的快捷键处理(兼�?Windows/Linux: Ctrl, Mac: Cmd/Meta�? + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的�? + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $("#new").click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $("#open").click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $("#save").click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + if (typeof window.output === 'function') { + window.output(null, saveAs); + } + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $("#previous").click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $("#next").click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.pasteFromClipboard(); + return; + } + + // CTRL/CMD + SHIFT + DEL (Windows) �?CMD + SHIFT + Backspace (Mac) - 清空 + if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#clean").click(); + return; + } + + // CTRL/CMD + DEL (Windows) �?CMD + Backspace (Mac) - 删除 + if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#delete").click(); + return; + } + + } + // 兼容三端�?modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta�? + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd�? + _keyctrl = true; + if (_keyc && _textEdit) { + //同时按下且在编辑状�? + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + } + } + if (event.keyCode == 67) { + _keyc = true; + if (_keyctrl & _textEdit) { + //同时按下且在编辑状�? + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + } + } + if (event.keyCode == 37) { + //left + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let x = currentCanvas.getActiveObject().get('left'); + x += v; + currentCanvas.getActiveObject().set('left', x); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + if (event.keyCode == 38) { + //up + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let y = currentCanvas.getActiveObject().get('top'); + y += v; + currentCanvas.getActiveObject().set('top', y); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + if (event.keyCode == 39) { + //right + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let x = currentCanvas.getActiveObject().get('left'); + x += v; + currentCanvas.getActiveObject().set('left', x); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + if (event.keyCode == 40) { + //down + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let y = currentCanvas.getActiveObject().get('top'); + y += v; + currentCanvas.getActiveObject().set('top', y); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + // 普通删除键(DEL键,没有按CTRL/CMD�? + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + // 使用 window.canvas 作为后备,确保在 eval() 加载 .jsc 时也能访问 + const currentCanvas = window.canvas || canvas; + const currentObjs = window.objs || objs; + if (!currentCanvas) { + return; + } + let actObjs = currentCanvas.getActiveObjects(); + let allObjs = currentCanvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item, currentCanvas);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + currentObjs.splice(i - 1, 1); + currentCanvas.remove(item); + flag = 1; + + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + currentCanvas.discardActiveObject(); + if (flag == 1) { + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + currentCanvas.renderAll(); + if (typeof window.handleObjectSelected === 'function') { + window.handleObjectSelected(); + } + } + } +}); +// pre_add_image 已在 design1.js 中定义为全局变量 +$("#addPic").click(function () { + OpenDialog(); + function OpenDialog() { + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showOpenDialog({ + title: language_str("selectPic"), + buttonLabel: language_str("comf"), + filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] + }).then(function(result) { + if (result.canceled) return; + var url = (result.files && result.files[0]) ? URL.createObjectURL(result.files[0]) : (result.filePaths && result.filePaths[0]); + if (!url) return; + fabric.Image.fromURL(url, function (image) { + //image.left = pointer.x; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image; + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 1; + }); + //canvas.add(image); + }); + }).catch(err => { + }) + } +}); +$("#addOutPic").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 2; +}); +$("#addText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 3; +}); + +$("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; +}); +$("#addOutText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 4; +}); +$("#addRect").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 5; +}); +$("#addCircle").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 6; +}); +$("#addLine").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 7; +}); +$("#addQrCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 8; +}); +$("#addBarCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 9; +}); +$("#addCounter").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + addState = 10; +}); +$("#version_change").click(function () { + if (bg_version == 1) { + bg_version = 2; + } else { + bg_version = 1; + } + // 类型改变 + $("#source_front").attr("src", soonAsset("front_bg") + bg_version + "_1.png"); + canvas.renderAll(); + +}); +$("#front_side").click(function () { + $(this).addClass("ui-button-active"); + $("#back_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").show(); + $(".canvas-container:eq(1)").hide(); + canvas = canvas1; + objs = objs1; + background_image = background_image1; + window.canvas = canvas; + window.objs = objs; + window.background_image = background_image; + next_objs = next_objs1; + pre_objs = pre_objs1; + next_json = next_json1; + pre_json = pre_json1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + if (typeof window.updateList === 'function') { + window.updateList(); + } + + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//背景 +}); +$("#back_side").click(function () { + $(this).addClass("ui-button-active"); + $("#front_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").hide(); + $(".canvas-container:eq(1)").show(); + canvas = canvas2; + objs = objs2; + background_image = background_image2; + window.canvas = canvas; + window.objs = objs; + window.background_image = background_image; + next_objs = next_objs2; + pre_objs = pre_objs2; + next_json = next_json2; + pre_json = pre_json2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + if (typeof window.updateList === 'function') { + window.updateList(); + } + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" +}); +$("#top_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinY = getCoordsMinY(coords); + let bgMinY = getCoordsMinY(background_image.getCoords(true)); + let deltaY = bgMinY - objMinY; + obj.set("top", obj.get("top") + deltaY); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最小Y值 + let minTop = Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + if (itemMinY < minTop) { + minTop = itemMinY; + } + } + // 将所有对象对齐到最小Y值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + let deltaY = minTop - itemMinY; + item.set("top", item.get("top") + deltaY); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + +}); +$("#bottom_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMaxY = getCoordsMaxY(coords); + let bgMaxY = getCoordsMaxY(background_image.getCoords(true)); + let deltaY = bgMaxY - objMaxY; + obj.set("top", obj.get("top") + deltaY); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最大Y值 + let maxBottom = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxY = getCoordsMaxY(coords); + if (itemMaxY > maxBottom) { + maxBottom = itemMaxY; + } + } + // 将所有对象对齐到最大Y值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxY = getCoordsMaxY(coords); + let deltaY = maxBottom - itemMaxY; + item.set("top", item.get("top") + deltaY); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + +}); +$("#center_y_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinY = getCoordsMinY(coords); + let objMaxY = getCoordsMaxY(coords); + let objCenterY = (objMinY + objMaxY) / 2; + let bgCoords = background_image.getCoords(true); + let bgMinY = getCoordsMinY(bgCoords); + let bgMaxY = getCoordsMaxY(bgCoords); + let bgCenterY = (bgMinY + bgMaxY) / 2; + let deltaY = bgCenterY - objCenterY; + obj.set("top", obj.get("top") + deltaY); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的中心Y值 + let minTop = Infinity; + let maxBottom = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + let itemMaxY = getCoordsMaxY(coords); + if (itemMinY < minTop) minTop = itemMinY; + if (itemMaxY > maxBottom) maxBottom = itemMaxY; + } + let groupCenterY = (minTop + maxBottom) / 2; + // 将所有对象对齐到中心Y值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + let itemMaxY = getCoordsMaxY(coords); + let itemCenterY = (itemMinY + itemMaxY) / 2; + let deltaY = groupCenterY - itemCenterY; + item.set("top", item.get("top") + deltaY); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#left_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinX = getCoordsMinX(coords); + let bgMinX = getCoordsMinX(background_image.getCoords(true)); + let deltaX = bgMinX - objMinX; + obj.set("left", obj.get("left") + deltaX); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最小X值 + let minLeft = Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + if (itemMinX < minLeft) { + minLeft = itemMinX; + } + } + // 将所有对象对齐到最小X值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + let deltaX = minLeft - itemMinX; + item.set("left", item.get("left") + deltaX); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#right_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMaxX = getCoordsMaxX(coords); + let bgMaxX = getCoordsMaxX(background_image.getCoords(true)); + let deltaX = bgMaxX - objMaxX; + obj.set("left", obj.get("left") + deltaX); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最大X值 + let maxRight = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxX = getCoordsMaxX(coords); + if (itemMaxX > maxRight) { + maxRight = itemMaxX; + } + } + // 将所有对象对齐到最大X值 + let groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxX = getCoordsMaxX(coords); + let deltaX = maxRight - itemMaxX; + item.set("left", item.get("left") + deltaX); + item.setCoords(); + groupItems.push(item); + } + //BUG1 + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#center_x_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinX = getCoordsMinX(coords); + let objMaxX = getCoordsMaxX(coords); + let objCenterX = (objMinX + objMaxX) / 2; + let bgCoords = background_image.getCoords(true); + let bgMinX = getCoordsMinX(bgCoords); + let bgMaxX = getCoordsMaxX(bgCoords); + let bgCenterX = (bgMinX + bgMaxX) / 2; + let deltaX = bgCenterX - objCenterX; + obj.set("left", obj.get("left") + deltaX); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的中心X值 + let minLeft = Infinity; + let maxRight = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + let itemMaxX = getCoordsMaxX(coords); + if (itemMinX < minLeft) minLeft = itemMinX; + if (itemMaxX > maxRight) maxRight = itemMaxX; + } + let groupCenterX = (minLeft + maxRight) / 2; + // 将所有对象对齐到中心X值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + let itemMaxX = getCoordsMaxX(coords); + let itemCenterX = (itemMinX + itemMaxX) / 2; + let deltaX = groupCenterX - itemCenterX; + item.set("left", item.get("left") + deltaX); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#up_obj").click(function () {//上移,放到前�?数组中在后面 + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + //console.log(objs); + + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } +}); +$("#down_obj").click(function () {//下移,放到后�?数组中在前面 + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } +}); +$("#set_bg").click(function () {//设置为背�? + let obj = canvas.getActiveObject(); + let idx = getIndex(obj); + const scaleX = 2787 / obj.width + const scaleY = 2787 / obj.height + // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 + const scaledWidth = obj.width * scaleX; + const scaledHeight = obj.height * scaleY; + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - scaledWidth) / 2, + top: ($('#canvas1').height() - scaledHeight) / 2, + }); + $('#text_w').val(2787) + $('#text_h').val(2787) + $('#text_x').val(0) + $('#text_y').val(0) + if (obj.get("selectable")) { + obj.set("selectable", false); + $("#lock_img").attr("src", soonAsset("lock20.png")) + } + canvas.sendToBack(obj); + canvas.bringForward(obj); + canvas.renderAll(); + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(0); + } +}); + +// ========================================== +// onStateRestored 函数在 core.js 中定义并附加到 window 对象 +// ========================================== +$("#previous").click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + + if (step.val == 0) return; + + step.val--; + + // 判断当前是正面还是背面 + if ($("#front_side").hasClass("ui-button-active")) { + // 1. 恢复业务数据 + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + // 2. 恢复画布 (使用回调函数) + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson1[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); // 图片加载完才执行这一行 + } + window._isApplyingHistory = false; + }); + } else { + // 背面逻辑 + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } +}); +// ========================================== +$("#next").click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + + // 获取当前历史记录的最大长度 + let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; + + if (step.val >= maxLen - 1) return; + + step.val++; + + if ($("#front_side").hasClass("ui-button-active")) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson1[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); // 图片加载完才执行这一行 + } + window._isApplyingHistory = false; + }); + } else { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } +}); + +$("#clean").click(function () { + objs.splice(0, objs.length); + + let allObjs = canvas.getObjects(); + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item); + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}); +$("#rotate").click(function () { + let obj = canvas.getActiveObject(); + obj.rotate(Math.round((obj.get("angle") + 90) % 360)); + $("#text_r").val(obj.get("angle")); + canvas.renderAll(); + updateControls(); + window.recordState(); +}); +$('#vertical_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipY", !obj.get("flipY")); + canvas.renderAll(); + updateControls(); + window.recordState(); +}); +$('#align_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipX", !obj.get("flipX")); + canvas.renderAll(); + updateControls(); + window.recordState(); +}); +$("#text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�?恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�?恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + window.recordState(); + canvas.renderAll(); +}) +$('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + window.recordState(); + canvas.renderAll(); +}) +$('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + window.recordState(); + canvas.renderAll(); +}) + +$("#out_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�?恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#lock").off('click').on('click', function (e) { + e.stopPropagation(); + if (!canvas.getActiveObject()) return; + let obj = canvas.getActiveObject(); + // 检查是否已锁定(通过lockMovementX判断,因为锁定时会设置所有lock属性) + let isLocked = obj.get("lockMovementX") === true; + let shouldLock = !isLocked; + + // 锁定/解锁所有编辑操作,但保持选中状态(不取消选中) + obj.set({ + lockMovementX: shouldLock, + lockMovementY: shouldLock, + lockRotation: shouldLock, + lockScalingX: shouldLock, + lockScalingY: shouldLock, + lockSkewingX: shouldLock, + lockSkewingY: shouldLock, + // 锁定时不可选中,解锁时可选中(但保持当前选中状态) + selectable: !shouldLock, + evented: !shouldLock + }); + if (shouldLock) { + $("#lock_img").attr("src", soonAsset("lock20.png"))//已锁定 + } else { + $("#lock_img").attr("src", soonAsset("unlock20.png"))//已解锁 + } + updateControls(); + window.recordState(); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas.renderAll(); +}); +$("#text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#circle_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#out_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#rect_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#rect_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#circle_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#circle_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); + +// saveAs 在 output.js 中定义(含辅助线清理);ui 内勿重复定义以免覆盖 +$("#copy").click(function () { + window.copySelection(); +}) +// 粘贴 +$("#paste").click(function () { + window.pasteFromClipboard(); +}) + +// 保存 (保持你原有的保存逻辑不变) +$("#save").click(function () { + if (typeof window.output === 'function') { + window.output(); // 这里调用你的 output 函数 + } + return; + // ... (下面是你原有的被 return 截断的代码,建议以后清理掉,但这里我按要求保�? + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + // ... (省略你原来那段很长的保存代码) ... +}); + +function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); +} +function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")] +} +// fullPath 和 saveHistory 已在 output.js 中定义 +$("#open").click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件�? + if (typeof window.output === 'function') { + window.output(OpenDialog); + } + layer.close(index); + + }, function (index) { + OpenDialog(); + }); + function OpenDialog() { + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showOpenDialog({ + title: "请选择文件", + buttonLabel: language_str("comf"), + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }).then(function(result) { + if (result.canceled) return; + var filePath = result.filePaths && result.filePaths[0]; + var file = (result.files && result.files[0]) || result.file; + function onData(j) { + var nameOrPath = (file && file.name) ? file.name : filePath; + var type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + if (type == 2) { + if (ipcRenderer) ipcRenderer.send('open-design-page', nameOrPath, type); + else if (window.platformBridge) window.platformBridge.openDesignPage(nameOrPath, type); + return; + } + if (typeof window.openFile === 'function') { + window.openFile(nameOrPath, j); + } + } + if (file && file.text) { + file.text().then(function(t) { onData(JSON.parse(t)); }).catch(function() {}); + return; + } + if (!filePath) return; + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + var fsData = fs.readFileSync(filePath); + onData(JSON.parse(fsData.toString())); + } + }).catch(function(err) {}); + } +}); +//let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; +let t_objs1 = canvas1.getObjects(); +let t_objs2 = canvas2.getObjects(); +for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]); +} +for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]); +} +objs2 = []; +objs1 = []; +step1.val = 0; +step2.val = 0; +recordJson1 = []; +recordJson2 = []; +recordObjs1 = []; +recordObjs2 = []; +if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; +} else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; +} +openAs.name = ""; +canvas1.renderAll(); +canvas2.renderAll(); +if (typeof window.updateList === 'function') { + window.updateList(); +} +$("#new").click(function () { + if (step.val == 0) { + if (ipcRenderer) ipcRenderer.send('open-first-page'); + else if (window.platformBridge) window.platformBridge.openFirstPage(); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件�? + if (typeof window.output === 'function') { + window.output(function() { if (ipcRenderer) ipcRenderer.send('open-first-page'); else if (window.platformBridge) window.platformBridge.openFirstPage(); }); + } + }, function (index) { + if (ipcRenderer) ipcRenderer.send('open-first-page'); + else if (window.platformBridge) window.platformBridge.openFirstPage(); + }); +}); + +// =========================================================== +$("#text_y").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + if ($("#text_y").val() == "") { + $("#text_y").val(0) + } + let val = background_image.top; + if ($("#text_y").val() != "") { + val = background_image.top + parseInt($("#text_y").val()); + } + canvas.getActiveObject().set("top", val) + canvas.renderAll(); + window.recordState(); +}) +$("#text_x").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + if ($("#text_x").val() == "") { + $("#text_x").val(0) + } + let val = background_image.left; + if ($("#text_x").val() != "") { + val = background_image.left + parseInt($("#text_x").val()); + } + canvas.getActiveObject().set("left", val) + canvas.renderAll(); + window.recordState(); +}) +$("#text_w").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + let val = "0"; + if ($("#text_w").val() != "") { + val = $("#text_w").val(); + } + let idx = getIndex(canvas.getActiveObject()); + if (idx <= 0 || !objs[idx - 1]) return; + let type = objs[idx - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("width", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleX", parseInt(val) / canvas.getActiveObject().get("width")) + } + + canvas.renderAll(); + window.recordState(); +}) +$("#text_h").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + let idx = getIndex(canvas.getActiveObject()); + if (idx <= 0 || !objs[idx - 1]) return; + if (objs[idx - 1].type == 7) { + if ($("#text_h").val() == "") { + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")); + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#text_h").val())); + canvas.renderAll(); + return; + } + let val = "0"; + if ($("#text_h").val() != "") { + val = $("#text_h").val(); + } + let type = objs[idx - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("height", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleY", parseInt(val) / canvas.getActiveObject().get("height")) + } + + canvas.renderAll(); + window.recordState(); +}) +$("#text_r").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + if ($("#text_r").val() == "") { + $("#text_r").val("0") + } + $("#text_r").val(parseInt($("#text_r").val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($("#text_r").val())) + canvas.renderAll(); + window.recordState(); +}) +$("#out_pic_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_pic_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) + +$("#text_black").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].black = true; + } else { + objs[idx - 1].black = false; + } + window.recordState(); +}); +$("#out_text_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_text_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#qrcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#qrcode_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#qrcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64($("#qrcode_val").val(), { padding: 0, foreground: $(this).val() }); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + item.setSrc(qr, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#qrcode_val").bind('input propertychange', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#barcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#barcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#barcode_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#text_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#text_back_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + window.recordState(); +}) +$("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); + canvas.renderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#out_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#out_text_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#out_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#out_text_back_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + window.recordState(); +}) +// 监听合成文本输入框 (占位符) +$("#out_text_text").on('input propertychange', function () { + let val = $(this).val(); + + // 【核心修改】如果值为空,强制给 2 个空格 + if (val === "") { + val = " "; + } + + let obj = canvas.getActiveObject(); + if (obj) { + obj.set("text", val); + obj.setCoords(); + canvas.requestRenderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); + } +}); +$("#rect_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#rect_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#rect_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#rect_stroke").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#rect_stroke_width").bind('input propertychange', function () { + if ($("#rect_stroke_width").val() == "") { + $("#rect_stroke_width").val("0") + } else { + $("#rect_stroke_width").val(parseInt($("#rect_stroke_width").val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#rect_stroke_width").val())); + canvas.renderAll(); + window.recordState(); +}) +$("#rect_radius").bind('input propertychange', function () { + if ($("#rect_radius").val() == "") { + $("#rect_radius").val("0") + } else { + $("#rect_radius").val(parseInt($("#rect_radius").val())) + } + canvas.getActiveObject().set("rx", parseInt($("#rect_radius").val())); + canvas.getActiveObject().set("ry", parseInt($("#rect_radius").val())); + canvas.renderAll(); + window.recordState(); +}) +$("#circle_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#circle_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#circle_stroke").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#circle_stroke_width").bind('input propertychange', function () { + if ($("#circle_stroke_width").val() == "") { + $("#circle_stroke_width").val("0") + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#circle_stroke_width").val())); + canvas.renderAll(); + window.recordState(); +}) +$("#counter_start").bind('input propertychange', function () { + canvas.getActiveObject().set("text", $("#counter_start").val()); + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].start = $(this).val(); + canvas.renderAll(); + window.recordState(); +}) +$("#counter_add").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].add = $(this).val(); + canvas.renderAll(); + window.recordState(); +}) +$("#counter_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#counter_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + window.recordState(); +}) +$("#line_color").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#line_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#line_dist").bind('input propertychange', function () { + if ($("#line_dist").val() == "") { + $("#line_dist").val(0) + } + canvas.getActiveObject().set("strokeDashArray", [parseInt($("#line_dist").val()), parseInt($("#line_dist").val())]) + canvas.renderAll(); + window.recordState(); +}) +// 监听普通文本输入 (占位符) +$("#text_text").on('input propertychange', function () { + // 1. 获取输入框的值,不要用 trim(),否则无法输入空格 + let val = $(this).val(); + + // 2. 核心修改:只有当删光时,才给 4 个空格作为占位符 + if (val === "") { + val = " "; // 4个空格,比原来的2个更宽,更好点 + } + // 3. 更新对象 + canvas.getActiveObject().set("text", val); + canvas.requestRenderAll(); // 使用 requestRenderAll 性能更好 + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}); +$("#barcode_val").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].val = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}) +$("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val(); + let displayValue = true; + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + displayValue = false; + } else { + objs[idx - 1].show = false; + displayValue = true; + } + //val = 0; // Remove this hack + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val * dpi / 72, font: objs[idx - 1].font, displayValue: displayValue }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val * dpi / 72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}) +$('#count_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); +$('#text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); + +$('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); + +$('#language_select').on('change', function () {//语言选择 + langua_ge($(this).find('option:selected').val()); + s_lan = $(this).find('option:selected').val(); + localStorage.setItem("lang", s_lan); +}); +$('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].font = $(this).find('option:selected').text(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); + //window.recordState(); +}); +$('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); +$(".obj_list").on("click", "div", function () { + let idx = objs.length - 1 - $(this).index(); + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } +}); +$("#exceed").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].exceed = true; + } else { + objs[idx - 1].exceed = false; + } + window.recordState(); +}); +$("#autoWrap").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].autoWrap = true; + } else { + objs[idx - 1].autoWrap = false; + } + window.recordState(); +}); +$("#fixSize").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].fixSize = true; + } else { + objs[idx - 1].fixSize = false; + } + window.recordState(); +}); +$("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + window.recordState(); +}); + +// =========================================================== +$("#text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + window.recordState(); + canvas.renderAll() +}) +$('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + window.recordState(); + canvas.renderAll() +}) +$('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + window.recordState(); + canvas.renderAll() +}) + +$("#out_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +// =========================================================== +$("#delete").click(function () { + let actObjs = canvas.getActiveObjects(); + let allObjs = canvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1); + canvas.remove(item); + flag = 1; + + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas.discardActiveObject(); + if (flag == 1) { + window.recordState(); + canvas.renderAll(); + } +}); +// 移至上层和移至下层:先取消所有绑定(包括第一处的绑定),然后重新绑定(确保只绑定一次) +// 注意:第一处的事件绑定(第 842 行和第 861 行)会被这里的 off('click') 取消 +$("#up_obj").off('click').on('click', function () {//上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } +}); +$("#down_obj").off('click').on('click', function () {//下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } +}); +$("#set_bg").click(function () {//设置为背景 + let obj = canvas.getActiveObject(); + let idx = getIndex(obj); + const scaleX = 2787 / obj.width + const scaleY = 2787 / obj.height + // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 + const scaledWidth = obj.width * scaleX; + const scaledHeight = obj.height * scaleY; + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - scaledWidth) / 2, + top: ($('#canvas1').height() - scaledHeight) / 2, + }); + $('#text_w').val(2787) + $('#text_h').val(2787) + $('#text_x').val(0) + $('#text_y').val(0) + // 设为背景时,锁定所有编辑操作,并设置为不可选中 + obj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + $("#lock_img").attr("src", soonAsset("lock20.png")) + canvas.sendToBack(obj); + canvas.bringForward(obj); + canvas.renderAll(); + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + + // 确保背景图(索引0)的锁定属性被正确设置 + setTimeout(() => { + let bgObj = canvas.getObjects()[0]; + if (bgObj) { + bgObj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + canvas.renderAll(); + } + }, 100); + + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(0); + } +}); +// 重复的lock点击事件已移除,使用第一处的绑定 +$("#text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#circle_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#out_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#rect_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#rect_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#circle_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#circle_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); + +// =========================================================== +$("#about").click(async function () { + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
' + + '
' + + '
Soon Design
' + + // 【修改点】这里使用了刚才获取到的 version 变量 + '
' + language_str("vers") + ' ' + version + '
' + + '
' + language_str("copyright") + '
' + + '
' + }); +}) +$("#help").click(function() { + if (window.platformBridge && window.platformBridge.openHelp) window.platformBridge.openHelp(); + else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-help-file'); +}); + +// =========================================================== +$('#display').on('click', function () { + // 类型改变 + fabric.Image.fromURL(soonAsset('front_bg') + bg_version + '_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + + fabric.Image.fromURL(soonAsset('back_bg_1.png'), function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + fabric.Image.fromURL(soonAsset('op_1.png'), function (i3) { + i3.left = background_image.left; + i3.top = background_image.top; + let img3 = i3; + if (typeof window.display_func === 'function') { + window.display_func(img1, img2, img3); + } + }) + }) + }) +}); +$("#output").click(function () { + // 另存为:传递 saveAs 函数作为第二个参数 + if (typeof window.output === 'function') { + window.output(null, saveAs); // 调用output函数,使用saveAs进行另存为 + } +}); + +// 确保移至上层和移至下层的事件绑定正确(在文件末尾再次确认) +// 取消所有绑定,然后重新绑定第二处的实现(第 2171 行和第 2189 行) +$(document).ready(function () { + setTimeout(function () { + $("#up_obj").off('click').on('click', function () { + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } + }); + $("#down_obj").off('click').on('click', function () { + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } + }); + }, 100); +}); diff --git a/lib/design2-back.js b/frontend-electron/js/design2-back.js similarity index 98% rename from lib/design2-back.js rename to frontend-electron/js/design2-back.js index bb2af76..ab42b03 100644 --- a/lib/design2-back.js +++ b/frontend-electron/js/design2-back.js @@ -1,6144 +1,6144 @@ - - function desEncrypt(str, key = 'df6a551ca43181fc485f3043bcdd2fbc') { - var APIFMS - try { - var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key) - var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7 - }) - APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext) - } catch (err) { - console.log('des 加密 -------------------------') - console.log(err) - } - return APIFMS - } - - (function (fabric) { - fabric.CurvedText = fabric.util.createClass(fabric.Object, { - type: 'curved-text', - diameter: 250, - kerning: 0, - text: '', - flipped: false, - fill: '#000', - fontFamily: 'Times New Roman', - fontSize: 24, // in px - fontWeight: 'normal', - fontStyle: '', // "normal", "italic" or "oblique". - cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), - strokeStyle: null, - strokeWidth: 0, - initialize: function (text, options) { - options || (options = {}); - if (typeof text === 'object' && text) { - for (let key in text) { - this[key] = text[key] - } - } else { - this.text = text; - } - this.callSuper('initialize', options); - this.set('lockUniScaling', true); - var canvas = this.getCircularText(); - this.cropCanvas(canvas); - this.set('width', canvas.width); - this.set('height', canvas.height); - }, - _getFontDeclaration: function () { - return [ - (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), - (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), - this.fontSize + 'px', - (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) - ].join(' '); - }, - cropCanvas: function (canvas) { - var ctx = canvas.getContext('2d'), - w = canvas.width, - h = canvas.height, - pix = { x: [], y: [] }, n, - imageData = ctx.getImageData(0, 0, w, h), - fn = function (a, b) { return a - b }; - for (var y = 0; y < h; y++) { - for (var x = 0; x < w; x++) { - if (imageData.data[((y * w + x) * 4) + 3] > 0) { - pix.x.push(x); - pix.y.push(y); - } - } - } - // 如果没有找到任何非透明像素,直接返回,不进行裁剪 - if (pix.x.length === 0 || pix.y.length === 0) { - return; - } - pix.x.sort(fn); - pix.y.sort(fn); - n = pix.x.length - 1; - w = pix.x[n] - pix.x[0]; - h = pix.y[n] - pix.y[0]; - // 确保宽度和高度是有效的正数 - if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { - return; - } - var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); - canvas.width = w; - canvas.height = h; - ctx.putImageData(cut, 0, 0); - }, - getCircularText: function () { - var text = this.text, - diameter = this.diameter, - flipped = this.flipped, - kerning = this.kerning, - fill = this.fill, - inwardFacing = true, - startAngle = 0, - canvas = fabric.util.createCanvasElement(), - ctx = canvas.getContext('2d'), - cw, // character-width - x, // iterator - clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise - if (flipped) { - // startAngle = 180; - inwardFacing = false; - } - startAngle *= Math.PI / 180; // convert to radians - var d = document.createElement('div'); - d.style.fontFamily = this.fontFamily; - d.style.fontSize = this.fontSize + 'px'; - d.style.fontWeight = this.fontWeight; - d.style.fontStyle = this.fontStyle; - d.textContent = text; - document.body.appendChild(d); - var textHeight = d.offsetHeight; - document.body.removeChild(d); - canvas.width = canvas.height = diameter; - ctx.font = this._getFontDeclaration(); - if (inwardFacing) { text = text.split('').reverse().join('') }; - ctx.translate(diameter / 2, diameter / 2); // Move to center - startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward - ctx.textBaseline = 'middle'; // Ensure we draw in exact center - ctx.textAlign = 'center'; // Ensure we draw in exact center - for (x = 0; x < text.length; x++) { - cw = ctx.measureText(text[x]).width; - startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; - } - ctx.rotate(startAngle); - for (x = 0; x < text.length; x++) { - cw = ctx.measureText(text[x]).width; // half letter - ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); - if (this.strokeStyle && this.strokeWidth) { - ctx.strokeStyle = this.strokeStyle; - ctx.lineWidth = this.strokeWidth; - ctx.miterLimit = 2; - ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); - } - ctx.fillStyle = fill; - ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); - ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter - } - return canvas; - }, - _set: function (key, value) { - switch (key) { - case 'scaleX': - this.fontSize *= value; - this.diameter *= value; - this.width *= value; - this.scaleX = 1; - if (this.width < 1) { this.width = 1; } - break; - case 'scaleY': - this.height *= value; - this.scaleY = 1; - if (this.height < 1) { this.height = 1; } - break; - default: - this.callSuper('_set', key, value); - break; - } - }, - _render: function (ctx) { - var canvas = this.getCircularText(); - this.cropCanvas(canvas); - this.set('width', canvas.width); - this.set('height', canvas.height); - ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); - this.setCoords(); - }, - toObject: function (propertiesToInclude) { - return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); - } - }); - fabric.CurvedText.fromObject = function (object, callback, forceAsync) { - return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); - }; - })(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); - - const remote = require('@electron/remote') - var path = require('path'); - // 使用用户数据目录,避免写入需要管理员权限 - const exePath = remote.app.getPath('userData') - const { ipcRenderer } = require('electron') - const { dialog } = require('@electron/remote') - var jrQrcode = require('jr-qrcode') - var JsBarcode = require('jsbarcode') - ipcRenderer.send('get-sys-fonts') - ipcRenderer.send('get-scale-rate') - const { clipboard } = require('electron') - const dpi = 300 - // 内部剪贴板 - let clipboardData = { - objects: null, - offset: 10 // 粘贴位置偏移量 - }; - //var encrypt = new JSEncrypt(); - //encrypt.setPublicKey(`-----BEGIN PUBLIC KEY----- - //MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDAbKQ46WevL9PapJ+AlZqSzczkTovu7STH - //FmZmIJcNrwPPEWRR2PliUvtS28V+gNMWlu3wNWFAS5KGDbTtndIrP3hnlp9BDIcAcdNuJUM7 - //yJfAYBhtkqvj45Orvdd2RYRxhkQb34uTNXpM+biMH6Wd0wykNb9/VBNM0C12KswEdwIBEQ== - //-----END PUBLIC KEY-----`); - layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { - let myDate = new Date() - let s_lan = '' - ipcRenderer.send('get-sys-language') - /* if(myDate.getFullYear()>2021 && myDate.getMonth() >= 1 && myDate.getDate() > 15){ - - }*/ - function GetFile() { - const params = new URLSearchParams(window.location.search); - return params - } - function langua_ge(lan = 'zh') { - $("[language='m']").each(function (i) { - $(this).html($(this).attr(lan)) - }) - - $("[language='t']").each(function (i) { - $(this).attr('title', $(this).attr(lan)) - }) - } - function language_str(str) { - let t = { - whetherSave: { zh: '是否保存当前文件?', ozh: '是否保存當前文件?', en: 'Do you want to save the current file?' }, - save: { zh: '保存', ozh: '保存', en: 'Save' }, - noSave: { zh: '不保存', ozh: '不保存', en: 'No saving' }, - cancel: { zh: '取消', ozh: '取消', en: 'Cancel' }, - saveSucc: { zh: '保存成功至', ozh: '保存成功至', en: 'Save successfully to' }, - saveFile: { zh: '保存文件', ozh: '保存文件', en: 'Save file' }, - display: { zh: '预览', ozh: '預覽', en: 'Display' }, - output: { zh: '导出', ozh: '導出', en: 'Export' }, - bg: { zh: '背景', ozh: '背景', en: 'Background' }, - selectPic: { zh: '请选择图片', ozh: '請選擇圖片', en: 'Please select a picture' }, - selectPic: { zh: '请选择文件', ozh: '請選擇文件', en: 'Please select a file' }, - img: { zh: '圖片', ozh: '圖片', en: 'Image' }, - simg: { zh: '合成圖片', ozh: '合成圖片', en: 'Synthesized Image' }, - text: { zh: '文本', ozh: '文本', en: 'Text' }, - "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, - stext: { zh: '合成文本', ozh: '合成文本', en: 'Synthesized Text' }, - rect: { zh: '矩形', ozh: '矩形', en: 'Rectangle' }, - circ: { zh: '圓形', ozh: '圓形', en: 'Circle' }, - line: { zh: '直線', ozh: '直線', en: 'Line' }, - qrc: { zh: '二維碼', ozh: '二維碼', en: 'QR code' }, - barc: { zh: '條形碼', ozh: '條形碼', en: 'Barcode' }, - counter: { zh: '計數器', ozh: '计数器', en: 'Counter' }, - about: { zh: '关于Soon Design', ozh: '關於Soon Design', en: 'About Soon Design' }, - vers: { zh: '版本', ozh: '版本', en: 'Version' }, - comf: { zh: '确认', ozh: '確認', en: 'Select' }, - copyright: { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } - } - return t[str][s_lan] - } - - var $ = layui.$ - var layer = layui.layer, - slider = layui.slider, - colorpicker = layui.colorpicker - let addState = 0 //0是不添加 - let background_image, background_image1, background_image2 - let bg_version = 1 //front_bg1.png front_bg2.png - var zoom = 0 - $('#canvas1').attr('height', document.body.clientHeight - 100) - $('#canvas2').attr('height', document.body.clientHeight - 100) - $('#object_attribute').css('height', '470px') - $('#obj_list').css('height', document.body.clientHeight - 520) - $('.obj_list').css('height', $('#obj_list').height() - 32) - - $('.main').show() - $('#canvas1').attr('width', $('#canvas-div').width()) - $('#canvas2').attr('width', $('#canvas-div').width()) - var openAs = { - _name: '', - set name(val) { - if (val == '') { - $('title').html('Soon Design') - } else { - $('title').html('Soon Design - ' + val) - } - this._name = val - }, - get name() { - return this._name - } - } //被打开的文件名 - let objs1 = [] - let objs2 = [] - let step1 = { val: 0 }, - step2 = { val: 0 } - let step = step1 - let objs = objs1 - let recordJson1 = [], - recordJson2 = [], - recordJson = recordJson1, - recordObjs1 = [], - recordObjs2 = [], - recordObjs = recordObjs1 - let pre_objs1 = [], - pre_objs2 = [], - next_objs1 = [], - next_objs2 = [], - pre_objs = pre_objs1, - next_objs = next_objs1 - let pre_json1 = [], - pre_json2 = [], - next_json1 = [], - next_json2 = [], - pre_json = pre_json1, - next_json = next_json1 - var canvas1 = new fabric.Canvas('canvas1') - var canvas2 = new fabric.Canvas('canvas2') - fabric.Object.prototype.objectCaching = false - var ctx1 = canvas1.getSelectionContext() - var ctx2 = canvas2.getSelectionContext() - let is_bgi_add = false - canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) - //console.log(canvas1.width / 2, canvas1.height / 2); - //console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200); - canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) - zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200 - - // 初始化标尺 - initRulers(); - $('#source_front').width(zoom * $('#source_front').width()) - $('#source_back').width(zoom * $('#source_back').width()) - $('.canvas-container:eq(1)').hide() - var canvas = canvas1 - fabric.Object.prototype.set({ - borderColor: '#9013FE', - cornerColor: '#9013FE', //激活状态角落图标的填充颜色 - cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 - borderOpacityWhenMoving: 1, - borderScaleFactor: 1, - cornerSize: 6, - cornerStyle: 'circle', //rect,circle - centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 - centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 - transparentCorners: false, //激活状态角落的图标是否透明 - rotatingPointOffset: 20, //旋转距旋转体的距离 - padding: 0 - }) - canvas1.preserveObjectStacking = true - canvas2.preserveObjectStacking = true - - var front_list = [] - ipcRenderer.on('font-list', (event, data) => { - const newFontList = [] - data.map((item) => { - if (item.indexOf('"') === 0) { - newFontList.push(item.replace(/^"|"$/g, '')) - } else { - newFontList.push(item) - } - }) - front_list = newFontList - for (let item of front_list) { - $('#text_font_family').append("') - $("#circle_text_font_family").append(""); - $('#out_text_font_family').append("') - $('#barcode_font_family').append("') - $('#count_font_family').append("') - } - }) - ipcRenderer.on('sys-lan', (event, data) => { - let lang = localStorage.getItem('lang') - if (lang) { - s_lan = lang - langua_ge(lang) - $('#language_select').val(lang) - return - } - switch (data) { - case 'zh-CN': - s_lan = 'zh' - langua_ge('zh') - $('#language_select').val('zh') - localStorage.setItem('lang', 'zh') - break - case 'zh': - s_lan = 'zh' - langua_ge('zh') - $('#language_select').val('zh') - localStorage.setItem('lang', 'zh') - break - case 'zh-TW': - s_lan = 'ozh' - langua_ge('ozh') - $('#language_select').val('ozh') - localStorage.setItem('lang', 'ozh') - break - case 'en': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-AU': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-CA': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-GB': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-NZ': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-US': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - case 'en-ZA': - s_lan = 'en' - langua_ge('en') - $('#language_select').val('en') - localStorage.setItem('lang', 'en') - break - } - }) - ipcRenderer.on('close', (event, message) => { - layer.confirm( - '' + language_str('whetherSave') + '', - { - //是否保存当前文件? - btn: [language_str('save'), language_str('noSave'), language_str('cancel')], //['保存', '不保存', '取消'] - btn3: function (index, layero) { } - }, - function (index, layero) { - output(() => ipcRenderer.send('run-close')) - return - let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let _g1 = [], - _g2 = [] - let left = 0, - top = 0 - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g1.push(fabric.util.object.clone(_o)) - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - ; (left = 0), (top = 0) - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g2.push(fabric.util.object.clone(_o)) - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } - if (openAs.name != '') { - //打开的文件 - let fs = require('fs') - o.soonType = 2 - fs.writeFileSync(openAs.name, JSON.stringify(o)) - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - saveHistory() - - ipcRenderer.send('run-close') - return - } - dialog - .showSaveDialog({ - title: language_str('saveFile'), //'保存文件' - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }) - .then((result) => { - if (result.filePath == '') { - ipcRenderer.send('run-close') - return - } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon' - } - let fs = require('fs') - o.soonType = 2 - fs.writeFileSync(result.filePath, JSON.stringify(o)) - openAs.name = result.filePath - layer.msg(language_str('saveSucc') + openAs.name) //保存成功至 - saveHistory() - ipcRenderer.send('run-close') - }) - .catch((err) => { - console.log() - }) - layer.close(index) - }, - function (index) { - ipcRenderer.send('run-close') - } - ) - }) - - addBackground() //添加背景 白色卡片 - - initAligningGuidelines(canvas1) - initAligningGuidelines(canvas2) - //canvas.setZoom(0.5) - //recordObjs1.push(JSON.stringify(objs1)); - //recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - //recordObjs2.push(JSON.stringify(objs2)); - //recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - canvas.renderAll() //重新渲染画布 - var zoomSlider = slider.render({ - elem: '#zoomSlide', //绑定元素 - min: 25, - max: 500, - value: 100, - change(val) { - canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100) - zoom = val / 100 - //$(".canvas-container").css("transform","scale(" + val / 100 + ")") - } - }) - window.rotate = function (id) { - let angle = $(`#${id}`).attr('angle') - 0 - angle += 90 - $(`#${id}`).css('transform', `rotate(${angle}deg)`) - $(`#${id}`).attr('angle', angle) - } - $('#display').on('click', function () { - // 类型改变 - fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_2.png', function (i1) { - i1.left = background_image.left - i1.top = background_image.top - let img1 = i1 - - fabric.Image.fromURL('./public/images/back_bg_2.png', function (i2) { - i2.left = background_image.left - i2.top = background_image.top - let img2 = i2 - fabric.Image.fromURL('./public/images/op_2.png', function (i3) { - i2.left = background_image.left - i2.top = background_image.top - let img3 = i3 - display_func(img1, img2, img3) - }) - }) - }) - }) - // 导出文件 - async function savePdf(buffer) { - let fs = require('fs'); - // 弹出保存对话框 - const { canceled, filePath } = await dialog.showSaveDialog({ - title: '保存文件', - defaultPath: path.join(exePath, 'output.pdf'), // 默认路径 - filters: [{ name: 'PDF 文件', extensions: ['pdf'] }] // 文件过滤器 - }); - - if (canceled) { - console.log('用户取消了保存操作'); - return; - } - - // 将Buffer写入PNG文件到用户选择的路径 - fs.writeFile(filePath, buffer, (err) => { - if (err) { - console.error('写入文件失败:', err); - } else { - layer.msg(language_str("saveSucc") + filePath);//'保存成功至' - console.log('PNG文件已保存至:', filePath); - } - }); - } - function display_func(img1, img2, img3) { - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $('#circle_text_control').hide() - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) //"背景" - canvas1.discardActiveObject().renderAll() - canvas2.discardActiveObject().renderAll() - let _objs1 = canvas1.getObjects() - let g1 = [] - let g3 = [] - let g5 = [] - let left = 0 //左裁剪 - let top = 0 //顶裁剪 - let black_left = 0 //黑色左裁剪 - let black_top = 0 //黑色顶裁剪 - let all_left = 0 //预览图左裁剪 - let all_top = 0 //预览图顶裁剪 - for (let item of _objs1) { - let idx = getIndex(item, canvas1) - //console.log("------"); - //console.log(item.getCoords()); - //console.log(getCoordsMinX(background_image.getCoords())); - //console.log(getCoordsMinX(item.getCoords())); - //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); - //console.log("------"); - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g3.push(fabric.util.object.clone(item)) - if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { - continue //计数器 或 合成文字 - } - if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { - continue //二维码 条形码 - } - if (idx != 0 && objs1[idx - 1].type == 2) { - continue //合成图片 - } - if ( - idx != 0 && - ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || - ( - objs1[idx - 1].type == 11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) || - //文字 && 黑色 - (objs1[idx - 1].type == 5 && //矩形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs1[idx - 1].type == 6 && //圆形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs1[idx - 1].type == 7 && //直线 - (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || - objs1[idx - 1].black == true) //勾选并入黑色图层 - // ( - // objs1[idx-1].type == 1//固定图片 - // && - // objs1[idx-1].black == true //勾选并入黑色图层 - // ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g5.push(fabric.util.object.clone(item)) - continue //黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g5.push(fabric.util.object.clone(item)) - } - - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g1.push(fabric.util.object.clone(item)) - } - let url1 = new fabric.Group(g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - - // g3.push(img1) - let url3 = new fabric.Group(g3).toDataURL({ - height: 648, - width: 1012, - top: all_top / zoom, - left: all_left / zoom - }) - let url5 = new fabric.Group(g5).toDataURL({ - height: 648, - width: 1012, - top: black_top / zoom, - left: black_left / zoom - }) - let _objs2 = canvas2.getObjects() - let g2 = [] - let g4 = [] - let g6 = [] - left = 0 - top = 0 - black_top = 0 - black_left = 0 - all_top = 0 - all_left = 0 - for (let item of _objs2) { - let idx = getIndex(item, canvas2) - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g4.push(fabric.util.object.clone(item)) - if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { - continue //黑色字或计数器 或 合成文字 - } - if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { - continue //二维码 条形码 - } - if (idx != 0 && objs2[idx - 1].type == 2) { - continue //合成图片 - } - if (idx != 0 && objs2[idx - 1].type == 10) { - continue //合成图片 - } - if ( - idx != 0 && - ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || - ( - objs2[idx - 1].type == 11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) || - //文字 && 黑色 - (objs2[idx - 1].type == 5 && //矩形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs2[idx - 1].type == 6 && //圆形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs2[idx - 1].type == 7 && //直线 - (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || - objs2[idx - 1].black == true) //勾选并入黑色图层 - // ( - // objs2[idx-1].type == 1//固定图片 - // && - // objs2[idx-1].black == true //勾选并入黑色图层 - // ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g6.push(fabric.util.object.clone(item)) - continue //黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g6.push(fabric.util.object.clone(item)) - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g2.push(fabric.util.object.clone(item)) - } - let url2 = new fabric.Group(g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let white_url1 = canvas1.getObjects()[0].toDataURL({ - height: 648, - width: 1012 - }) - // g4.push(img2) - let url4 = new fabric.Group(g4).toDataURL({ - height: 648, - width: 1012, - top: all_top / zoom, - left: all_left / zoom - }) - let url6 = new fabric.Group(g6).toDataURL({ - height: 648, - width: 1012, - top: black_top / zoom, - left: black_left / zoom - }) - let white_url2 = canvas2.getObjects()[0].toDataURL({ - height: 648, - width: 1012 - }) - //console.log(canvas.getObjects()) - let w = document.body.clientWidth * 0.7 + 'px' - let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' - let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' - - if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { - //高度超出,则按高度计算 - h = document.body.clientHeight * 0.8 + 'px' - h1 = document.body.clientHeight * 0.8 - 150 + 'px' - w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 - } - let fs = require('fs'); - const Buffer = require('buffer').Buffer; - const buffer3 = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); - const printPath3 = path.join(exePath, 'print3.png'); - fs.writeFileSync(printPath3, buffer3); - const buffer4 = Buffer.from(url4.replace(/^data:image\/\w+;base64,/, ""), 'base64'); - const printPath4 = path.join(exePath, 'print4.png'); - fs.writeFileSync(printPath4, buffer4); - // 创建 PDF 实例 - const pdf = new jspdf.jsPDF({ - orientation: 'landscape', - unit: 'px', - format: [1012, 648] - }); - let btns = [language_str('output')] - if ((objs1.length != 0) & (objs2.length == 0)) { - //只有正面 - let img = new Image() - img.src = printPath3 - - pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) - btns.push('打印正面') - } else if ((objs1.length == 0) & (objs2.length != 0)) { - //只有背面 - let img = new Image() - img.src = printPath4 - - pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) - btns.push('打印背面') - } else { - let img = new Image() - img.src = printPath3 - - pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) - - pdf.addPage() - let img2 = new Image() - img2.src = printPath4 - pdf.addImage(img2, 'JPEG', 0, 0, 1012, 648) - btns.push('打印正面') - btns.push('打印背面') - } - - var pdfBlob = pdf.output('blob') - var pdfBuffer = pdf.output('arraybuffer') - - layer.open({ - type: 1, - area: [w, h], - title: language_str('display'), //不显示标题栏"预览" - shadeClose: true, //点击遮罩关闭 - content: ` -
-
-
- -
- -
-
-
- -
- -
-
-
-
`, - btn: btns, //'导出' - btn1: function (index, layero) { - savePdf(Buffer.from(pdfBuffer)) - }, - btn2: function () { - if (btns[1] === '打印正面') { - printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' }) - } else { - printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }) - } - }, - btn3: function () { - //打印背面 - printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }) - } - }) - } - function output(callback = null, _save = save) { - // 类型改变 - fabric.Image.fromURL('./public/images/op_2.png', function (i1) { - i1.left = background_image.left - i1.top = background_image.top - let img1 = i1 - fabric.Image.fromURL('./public/images/op_2.png', function (i2) { - i2.left = background_image.left - i2.top = background_image.top - let img2 = i2 - - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) //"背景" - canvas1.discardActiveObject().renderAll() - canvas2.discardActiveObject().renderAll() - let _objs1 = canvas1.getObjects() - let g1 = [] - let g3 = [] - let g5 = [] //黑色图层 - let left = 0 //左裁剪 - let top = 0 //顶裁剪 - let black_left = 0 //黑色左裁剪 - let black_top = 0 //黑色顶裁剪 - let all_left = 0 //预览图左裁剪 - let all_top = 0 //预览图顶裁剪 - for (let item of _objs1) { - let idx = getIndex(item, canvas1) - //console.log("------"); - //console.log(item.getCoords()); - //console.log(getCoordsMinX(background_image.getCoords())); - //console.log(getCoordsMinX(item.getCoords())); - //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); - //console.log("------"); - - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g3.push(fabric.util.object.clone(item)) - if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { - continue //计数器 或 合成文字 - } - if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { - continue //二维码 条形码 - } - if (idx != 0 && objs1[idx - 1].type == 2) { - continue //合成图片 - } - if ( - idx != 0 && - ((objs1[idx - 1].type == 3 && item.get('fill') == '#000000') || - ( - objs1[idx - 1].type == 11 - && - item.get("fill") == "#000000" - //圆形文字 && 黑色 - ) || - //文字 && 黑色 - (objs1[idx - 1].type == 5 && //矩形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs1[idx - 1].type == 6 && //圆形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs1[idx - 1].type == 7 && //直线 - (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || - objs1[idx - 1].black == true) //勾选并入黑色图层 - // || - // ( - // objs1[idx-1].type == 1//固定图片 - // && - // objs1[idx-1].black == true //勾选并入黑色图层 - // ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g5.push(fabric.util.object.clone(item)) - continue //黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g5.push(fabric.util.object.clone(item)) - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g1.push(fabric.util.object.clone(item)) - } - let url1 = new fabric.Group(g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - g3.push(img1) - let url3 = new fabric.Group(g3).toDataURL({ - height: 648, - width: 1012, - top: all_top / zoom, - left: all_left / zoom - }) - let url5 = new fabric.Group(g5).toDataURL({ - height: 648, - width: 1012, - top: black_top / zoom, - left: black_left / zoom - }) - let _objs2 = canvas2.getObjects() - let g2 = [] - let g4 = [] - let g6 = [] //黑色图层 - left = 0 - top = 0 - black_left = 0 //黑色左裁剪 - black_top = 0 //黑色顶裁剪 - all_left = 0 //黑色左裁剪 - all_top = 0 //黑色顶裁剪 - for (let item of _objs2) { - let idx = getIndex(item, canvas2) - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g4.push(fabric.util.object.clone(item)) - if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { - continue //计数器 或 合成文字 - } - if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { - continue //二维码 条形码 - } - if (idx != 0 && objs2[idx - 1].type == 2) { - continue //合成图片 - } - if ( - idx != 0 && - ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || - ( - objs2[idx - 1].type ==11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) || - //文字 && 黑色 - (objs2[idx - 1].type == 5 && //矩形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs2[idx - 1].type == 6 && //圆形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs2[idx - 1].type == 7 && //直线 - (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || - objs2[idx - 1].black == true) //勾选并入黑色图层 - // ( - // objs2[idx-1].type == 1//固定图片 - // && - // objs2[idx-1].black == true //勾选并入黑色图层 - // ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g6.push(fabric.util.object.clone(item)) - continue //黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g6.push(fabric.util.object.clone(item)) - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g2.push(fabric.util.object.clone(item)) - } - let url2 = new fabric.Group(g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let white_url1 = canvas1.getObjects()[0].toDataURL({ - height: 648, - width: 1012 - }) - g4.push(img2) - let url4 = new fabric.Group(g4).toDataURL({ - height: 648, - width: 1012, - top: all_top / zoom, - left: all_left / zoom - }) - let url6 = new fabric.Group(g6).toDataURL({ - height: 648, - width: 1012, - top: black_top / zoom, - left: black_left / zoom - }) - let white_url2 = canvas2.getObjects()[0].toDataURL({ - height: 648, - width: 1012 - }) - //console.log(canvas.getObjects()) - let w = document.body.clientWidth * 0.7 + 'px' - let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' - let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' - - if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { - //高度超出,则按高度计算 - h = document.body.clientHeight * 0.8 + 'px' - h1 = document.body.clientHeight * 0.8 - 150 + 'px' - w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 - } - - let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } - if ((objs1.length != 0) & (objs2.length == 0)) { - op.flag = 2 - } else if ((objs1.length == 0) & (objs2.length != 0)) { - op.flag = 3 - } - op.frontColorPic = desEncrypt(url1) - op.backColorPic = desEncrypt(url2) - //op.frontBlPic = url5; - //op.backBlPic = url6; - op.frontBlackPic = desEncrypt(url5) - op.backBlackPic = desEncrypt(url6) - for (let item of _objs1) { - let idx = getIndex(item, canvas1) - if (idx == 0) continue //background-image - let type = objs1[idx - 1].type - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 1, - name: objs1[idx - 1].name, - FontColor: objs1[idx - 1].black ? '#000000' : '@', - transparency: item.get('opacity'), - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 3) { - //text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - } else if (type == 11) { - //circle_text - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs1[idx - 1].circleCenter || 0 - }); - } else if (type == 4) { - //out_text - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 3, - name: objs1[idx - 1].name, - FontFamily: item.get('fontFamily'), - DefaultText: item.get('text'), - FontBackgroudColor: item.get('textBackgroundColor'), - FontSize: parseInt(item.get('fontSize')), - FontColor: item.get('fill'), - Exceed: objs1[idx - 1].exceed, - AutoWrap: objs1[idx - 1].autoWrap, - FixSize: objs1[idx - 1].fixSize, - Bold: item.get('fontWeight') == 'bold', - Underline: item.get('underline'), - Italic: item.get('fontStyle') == 'italic', - Strikeout: item.get('linethrough'), - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)), - align: objs1[idx - 1].align - }) - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 4, - name: objs1[idx - 1].name, - FontColor: objs1[idx - 1].color, - DefaultText: objs1[idx - 1].val, - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 9) { - //条形码 - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 5, - name: objs1[idx - 1].name, - FontSize: parseInt(objs1[idx - 1].fontSize), - DefaultText: objs1[idx - 1].val, - HideText: objs1[idx - 1].show, - FontFamily: objs1[idx - 1].font, - FontColor: objs1[idx - 1].color, - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 10) { - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 6, - name: objs1[idx - 1].name, - FontFamily: item.get('fontFamily'), - FontSize: parseInt(item.get('fontSize')), - FontColor: item.get('fill'), - start: objs1[idx - 1].start, - add: parseInt(objs1[idx - 1].add), - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } - } - //obj2 - for (let item of _objs2) { - let idx = getIndex(item, canvas2) - if (idx == 0) continue //background-image - let type = objs2[idx - 1].type - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 1, - name: objs2[idx - 1].name, - FontColor: objs2[idx - 1].black ? '#000000' : '@', - transparency: item.get('opacity'), - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 3) { - //text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.backData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - } else if (type == 11) { - //circle_text - op.backData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs2[idx - 1].circleCenter || 0 - }); - } else if (type == 4) { - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 3, - name: objs2[idx - 1].name, - FontFamily: item.get('fontFamily'), - DefaultText: item.get('text'), - FontSize: parseInt(item.get('fontSize')), - FontBackgroudColor: item.get('textBackgroundColor'), - FontColor: item.get('fill'), - Exceed: objs2[idx - 1].exceed, - AutoWrap: objs2[idx - 1].autoWrap, - FixSize: objs2[idx - 1].fixSize, - Bold: item.get('fontWeight') == 'bold', - Underline: item.get('underline'), - Italic: item.get('fontStyle') == 'italic', - Strikeout: item.get('linethrough'), - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)), - align: objs2[idx - 1].align - }) - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 4, - name: objs2[idx - 1].name, - FontColor: objs2[idx - 1].color, - DefaultText: objs2[idx - 1].val, - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 9) { - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 5, - name: objs2[idx - 1].name, - DefaultText: objs2[idx - 1].val, - HideText: objs1[idx - 1].show, - FontSize: parseInt(objs2[idx - 1].fontSize), - FontFamily: objs2[idx - 1].font, - FontColor: objs2[idx - 1].color, - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 10) { - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 6, - name: objs2[idx - 1].name, - FontFamily: item.get('fontFamily'), - FontSize: parseInt(item.get('fontSize')), - FontColor: item.get('fill'), - start: parseInt(objs2[idx - 1].start), - add: parseInt(objs2[idx - 1].add), - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } - } - _save(op, callback) - //return false 开启该代码可禁止点击该按钮关闭 - }) - }) - } - $('#output').on('click', function () { - output(null, saveAs) - return - // 类型改变 - fabric.Image.fromURL('./public/images/op_2.png', function (i1) { - i1.left = background_image.left - i1.top = background_image.top - let img1 = i1 - fabric.Image.fromURL('./public/images/op_2.png', function (i2) { - i2.left = background_image.left - i2.top = background_image.top - let img2 = i2 - - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) //"背景" - canvas1.discardActiveObject().renderAll() - canvas2.discardActiveObject().renderAll() - let _objs1 = canvas1.getObjects() - let g1 = [] - let g3 = [] - let g5 = [] //黑色图层 - let left = 0 //左裁剪 - let top = 0 //顶裁剪 - let black_left = 0 //黑色左裁剪 - let black_top = 0 //黑色顶裁剪 - let all_left = 0 //预览图左裁剪 - let all_top = 0 //预览图顶裁剪 - for (let item of _objs1) { - let idx = getIndex(item, canvas1) - //console.log("------"); - //console.log(item.getCoords()); - //console.log(getCoordsMinX(background_image.getCoords())); - //console.log(getCoordsMinX(item.getCoords())); - //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); - //console.log("------"); - - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g3.push(fabric.util.object.clone(item)) - if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { - continue //计数器 或 合成文字 - } - if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { - continue //二维码 条形码 - } - if (idx != 0 && objs1[idx - 1].type == 2) { - continue //合成图片 - } - if ( - idx != 0 && - ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || - ( - objs2[idx - 1].type ==11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) || - //文字 && 黑色 - (objs1[idx - 1].type == 5 && //矩形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs1[idx - 1].type == 6 && //圆形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs1[idx - 1].type == 7 && //直线 - (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || - objs1[idx - 1].black == true) //勾选并入黑色图层 - // ( - // objs1[idx-1].type == 1//固定图片 - // && - // objs1[idx-1].black == true //勾选并入黑色图层 - // ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g5.push(fabric.util.object.clone(item)) - continue //黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g5.push(fabric.util.object.clone(item)) - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g1.push(fabric.util.object.clone(item)) - } - let url1 = new fabric.Group(g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - g3.push(img1) - let url3 = new fabric.Group(g3).toDataURL({ - height: 648, - width: 1012, - top: all_top / zoom, - left: all_left / zoom - }) - let url5 = new fabric.Group(g5).toDataURL({ - height: 648, - width: 1012, - top: black_top / zoom, - left: black_left / zoom - }) - let _objs2 = canvas2.getObjects() - let g2 = [] - let g4 = [] - let g6 = [] //黑色图层 - left = 0 - top = 0 - black_left = 0 //黑色左裁剪 - black_top = 0 //黑色顶裁剪 - all_left = 0 //黑色左裁剪 - all_top = 0 //黑色顶裁剪 - for (let item of _objs2) { - let idx = getIndex(item, canvas2) - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { - all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { - all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g4.push(fabric.util.object.clone(item)) - if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { - continue //计数器 或 合成文字 - } - if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { - continue //二维码 条形码 - } - if (idx != 0 && objs2[idx - 1].type == 2) { - continue //合成图片 - } - if ( - idx != 0 && - ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || - ( - objs2[idx - 1].type ==11 - && - (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) - //圆形文字 && 黑色 - ) || - //文字 && 黑色 - (objs2[idx - 1].type == 5 && //矩形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs2[idx - 1].type == 6 && //圆形 - (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 - (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 - ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 - (objs2[idx - 1].type == 7 && //直线 - (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || - objs2[idx - 1].black == true) //勾选并入黑色图层 - // ( - // objs2[idx-1].type == 1//固定图片 - // && - // objs2[idx-1].black == true //勾选并入黑色图层 - // ) - ) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { - black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { - black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g6.push(fabric.util.object.clone(item)) - continue //黑色文字 黑色矩形 黑色线条 黑色圆形 - } else if (idx == 0) { - g6.push(fabric.util.object.clone(item)) - } - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { - left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { - top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - g2.push(fabric.util.object.clone(item)) - } - let url2 = new fabric.Group(g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let white_url1 = canvas1.getObjects()[0].toDataURL({ - height: 648, - width: 1012 - }) - g4.push(img2) - let url4 = new fabric.Group(g4).toDataURL({ - height: 648, - width: 1012, - top: all_top / zoom, - left: all_left / zoom - }) - let url6 = new fabric.Group(g6).toDataURL({ - height: 648, - width: 1012, - top: black_top / zoom, - left: black_left / zoom - }) - let white_url2 = canvas2.getObjects()[0].toDataURL({ - height: 648, - width: 1012 - }) - //console.log(canvas.getObjects()) - let w = document.body.clientWidth * 0.7 + 'px' - let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' - let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' - - if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { - //高度超出,则按高度计算 - h = document.body.clientHeight * 0.8 + 'px' - h1 = document.body.clientHeight * 0.8 - 150 + 'px' - w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 - } - - let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } - if ((objs1.length != 0) & (objs2.length == 0)) { - op.flag = 2 - } else if ((objs1.length == 0) & (objs2.length != 0)) { - op.flag = 3 - } - op.frontColorPic = desEncrypt(url1) - op.backColorPic = desEncrypt(url2) - //op.frontBlPic = url5; - //op.backBlPic = url6; - op.frontBlackPic = desEncrypt(url5) - op.backBlackPic = desEncrypt(url6) - for (let item of _objs1) { - let idx = getIndex(item, canvas1) - if (idx == 0) continue //background-image - let type = objs1[idx - 1].type - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 1, - name: objs1[idx - 1].name, - FontColor: objs1[idx - 1].black ? '#000000' : '@', - transparency: item.get('opacity'), - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 3) { - //text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - } else if (type == 11) { - //circle_text - op.frontData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs1[idx - 1].circleCenter || 0 - }); - } else if (type == 4) { - //out_text - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 3, - name: objs1[idx - 1].name, - FontFamily: item.get('fontFamily'), - DefaultText: item.get('text'), - FontBackgroudColor: item.get('textBackgroundColor'), - FontSize: parseInt(item.get('fontSize')), - FontColor: item.get('fill'), - Exceed: objs1[idx - 1].exceed, - AutoWrap: objs1[idx - 1].autoWrap, - FixSize: objs1[idx - 1].fixSize, - Bold: item.get('fontWeight') == 'bold', - Underline: item.get('underline'), - Italic: item.get('fontStyle') == 'italic', - Strikeout: item.get('linethrough'), - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)), - align: objs1[idx - 1].align - }) - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 4, - name: objs1[idx - 1].name, - FontColor: objs1[idx - 1].color, - DefaultText: objs1[idx - 1].val, - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 9) { - //条形码 - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 5, - name: objs1[idx - 1].name, - FontSize: parseInt(objs1[idx - 1].fontSize), - DefaultText: objs1[idx - 1].val, - HideText: objs1[idx - 1].show, - FontFamily: objs1[idx - 1].font, - FontColor: objs1[idx - 1].color, - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 10) { - let xy = getAbsoluteXY(item) - op.frontData.push({ - type: 6, - name: objs1[idx - 1].name, - FontFamily: item.get('fontFamily'), - FontSize: parseInt(item.get('fontSize')), - FontColor: item.get('fill'), - start: objs1[idx - 1].start, - add: parseInt(objs1[idx - 1].add), - x: parseInt((xy[0] - background_image1.left).toFixed(0)), - y: parseInt((xy[1] - background_image1.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } - } - //obj2 - for (let item of _objs2) { - let idx = getIndex(item, canvas2) - if (idx == 0) continue //background-image - let type = objs2[idx - 1].type - if (type == 2) { - //out_pic - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 1, - name: objs2[idx - 1].name, - FontColor: objs2[idx - 1].black ? '#000000' : '@', - transparency: item.get('opacity'), - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 3) { - //text - /*2022-07-26 黑色并入黑色图层 - if(item.get("fill") == "#000000"){ - //纯黑 - op.backData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)) - }); - } - */ - } else if (type == 11) { - //circle_text - op.backData.push({ - "type" : 2, - "Text" : item.get("text"), - "FontFamily" : item.get("fontFamily"), - "FontSize" : parseInt(item.get("fontSize")), - "FontColor" : "#000000", - "FontBackgroudColor" : item.get("textBackgroundColor"), - "Bold" : item.get("fontWeight") == "bold", - "Underline" : item.get("underline"), - "Italic" : item.get("fontStyle") == "italic", - "Strikeout" : item.get("linethrough"), - "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), - "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), - "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), - "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), - "rotate" : parseInt(item.get("angle").toFixed(0)), - "circleCenter" : objs2[idx - 1].circleCenter || 0 - }); - } else if (type == 4) { - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 3, - name: objs2[idx - 1].name, - FontFamily: item.get('fontFamily'), - DefaultText: item.get('text'), - FontSize: parseInt(item.get('fontSize')), - FontBackgroudColor: item.get('textBackgroundColor'), - FontColor: item.get('fill'), - Exceed: objs2[idx - 1].exceed, - AutoWrap: objs2[idx - 1].autoWrap, - FixSize: objs2[idx - 1].fixSize, - Bold: item.get('fontWeight') == 'bold', - Underline: item.get('underline'), - Italic: item.get('fontStyle') == 'italic', - Strikeout: item.get('linethrough'), - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)), - align: objs2[idx - 1].align - }) - } else if (type == 8) { - //二维码 - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 4, - name: objs2[idx - 1].name, - FontColor: objs2[idx - 1].color, - DefaultText: objs2[idx - 1].val, - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 9) { - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 5, - name: objs2[idx - 1].name, - DefaultText: objs2[idx - 1].val, - HideText: objs1[idx - 1].show, - FontSize: parseInt(objs2[idx - 1].fontSize), - FontFamily: objs2[idx - 1].font, - FontColor: objs2[idx - 1].color, - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } else if (type == 10) { - let xy = getAbsoluteXY(item) - op.backData.push({ - type: 6, - name: objs2[idx - 1].name, - FontFamily: item.get('fontFamily'), - FontSize: parseInt(item.get('fontSize')), - FontColor: item.get('fill'), - start: parseInt(objs2[idx - 1].start), - add: parseInt(objs2[idx - 1].add), - x: parseInt((xy[0] - background_image2.left).toFixed(0)), - y: parseInt((xy[1] - background_image2.top).toFixed(0)), - h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), - w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), - rotate: parseInt(item.get('angle').toFixed(0)) - }) - } - } - dialog - .showSaveDialog({ - title: language_str('saveFile'), //'保存文件' - filters: [{ name: 'CS File Type', extensions: ['cs'] }] - }) - .then((result) => { - if (result.canceled) return - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.cs' - } - let fs = require('fs') - op.soonType = 2 - fs.writeFileSync(result.filePath, JSON.stringify(op)) - }) - .catch((err) => { - console.log() - }) - //return false 开启该代码可禁止点击该按钮关闭 - }) - }) - }) - var _keyctrl = false, - _keyc = false, - _textEdit = false, - _inputEdit = false - $('input').focus(function () { - console.log(1) - _inputEdit = true - }) - $('input').blur(function () { - _inputEdit = false - }) - $('textarea').focus(function () { - console.log(1) - _inputEdit = true - }) - $('textarea').blur(function () { - _inputEdit = false - }) - $('body').keyup(function (event) { - // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) - if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 - _keyctrl = false - } - if (event.keyCode == 67) { - _keyc = false - } - }) - $('body').keydown(function (event) { - // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) - const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 - - // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) - if (!_inputEdit && !_textEdit) { - // CTRL/CMD+N - 新建 - if (isModifierKey && event.keyCode == 78) { - event.preventDefault(); - $('#new').click(); - return; - } - // CTRL/CMD+O - 打开 - if (isModifierKey && event.keyCode == 79) { - event.preventDefault(); - $('#open').click(); - return; - } - // CTRL/CMD+S - 保存 - if (isModifierKey && !event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - $('#save').click(); - return; - } - // CTRL/CMD+SHIFT+S - 保存副本 - if (isModifierKey && event.shiftKey && event.keyCode == 83) { - event.preventDefault(); - output(null, saveAs); - return; - } - // CTRL/CMD+Z - 还原 - if (isModifierKey && !event.shiftKey && event.keyCode == 90) { - event.preventDefault(); - $('#previous').click(); - return; - } - // CTRL/CMD+Y - 重做 - if (isModifierKey && !event.shiftKey && event.keyCode == 89) { - event.preventDefault(); - $('#next').click(); - return; - } - // CTRL/CMD+C - 复制 - if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { - event.preventDefault(); - copySelection(); - return; - } - // CTRL/CMD+V - 粘贴 - if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { - event.preventDefault(); - pasteFromClipboard(); - return; - } - // CTRL/CMD+SHIFT+DEL - 清空 - if (isModifierKey && event.shiftKey && event.keyCode == 46) { - event.preventDefault(); - $('#clean').click(); - return; - } - // CTRL/CMD+DEL - 删除 - if (isModifierKey && !event.shiftKey && event.keyCode == 46) { - event.preventDefault(); - $('#delete').click(); - return; - } - } - // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) - if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) - _keyctrl = true - if (_keyc && _textEdit) { - //同时按下且在编辑状态 - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - //clipboard.writeText((clipboard.readText()+' ')) - } - } - if (event.keyCode == 67) { - _keyc = true - if (_keyctrl & _textEdit) { - //同时按下且在编辑状态 - setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) - //clipboard.writeText((clipboard.readText()+' ')) - } - } - if (event.keyCode == 37) { - //left - let v = -1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5 - } - let x = canvas.getActiveObject().get('left') - x += v - canvas.getActiveObject().set('left', x) - canvas.renderAll() - recordState() - updateControls() - } - if (event.keyCode == 38) { - //up - let v = -1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = -5 - } - let y = canvas.getActiveObject().get('top') - y += v - canvas.getActiveObject().set('top', y) - canvas.renderAll() - recordState() - updateControls() - } - if (event.keyCode == 39) { - //right - let v = 1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5 - } - let x = canvas.getActiveObject().get('left') - x += v - canvas.getActiveObject().set('left', x) - canvas.renderAll() - recordState() - updateControls() - } - if (event.keyCode == 40) { - //down - let v = +1 - if (_keyctrl || event.ctrlKey || event.metaKey) { - v = 5 - } - let y = canvas.getActiveObject().get('top') - y += v - canvas.getActiveObject().set('top', y) - canvas.renderAll() - recordState() - updateControls() - } - // 普通删除键(DEL键,没有按CTRL/CMD) - if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { - let actObjs = canvas.getActiveObjects() - let allObjs = canvas.getObjects() - let flag = 0 - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 - objs.splice(i - 1, 1) - canvas.remove(item) - flag = 1 - } - } - updateList() - canvas.discardActiveObject() - if (flag == 1) { - recordState() - canvas.renderAll() - handleObjectSelected() - } - } - }) - let pre_add_image - $('#addPic').click(function () { - OpenDialog() - function OpenDialog() { - dialog - .showOpenDialog({ - title: language_str('selectPic'), //请选择图片 - buttonLabel: language_str('comf'), //"确认" - filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] - }) - .then((result) => { - if (result.canceled) return - //console.log(result.filePaths) - fabric.Image.fromURL(result.filePaths[0], function (image) { - //image.left = pointer.x; - //image.top = pointer.y; - image.setSrc(image.toDataURL(), function (image) { - pre_add_image = image - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 1 - }) - //canvas.add(image); - //objs.push({type: 1}); - //canvas.renderAll(); - //updateList(); - //recordState(); - }) - }) - .catch((err) => { - console.log(err) - }) - } - }) - $('#addOutPic').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 2 - }) - $('#addText').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 3 - }) - $("#addCircleText").click(function () { - background_image.hoverCursor = "crosshair"; - $(this).css("background-color", "#646E76"); - - $("#addPic").css("background-color", ""); - $("#addOutPic").css("background-color", ""); - $("#addText").css("background-color", ""); - $("#addOutText").css("background-color", ""); - $("#addRect").css("background-color", ""); - $("#addCircle").css("background-color", ""); - $("#addLine").css("background-color", ""); - $("#addQrCode").css("background-color", ""); - $("#addBarCode").css("background-color", ""); - $("#addCounter").css("background-color", ""); - addState = 11; - }); - $('#addOutText').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 4 - }) - $('#addRect').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 5 - }) - $('#addCircle').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 6 - }) - $('#addLine').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 7 - }) - $('#addQrCode').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addBarCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 8 - }) - $('#addBarCode').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addCounter').css('background-color', '') - addState = 9 - }) - $('#addCounter').click(function () { - background_image.hoverCursor = 'crosshair' - $(this).css('background-color', '#646E76') - - $('#addPic').css('background-color', '') - $('#addOutPic').css('background-color', '') - $('#addText').css('background-color', '') - $("#addCircleText").css("background-color", ""); - $('#addOutText').css('background-color', '') - $('#addRect').css('background-color', '') - $('#addCircle').css('background-color', '') - $('#addLine').css('background-color', '') - $('#addQrCode').css('background-color', '') - $('#addBarCode').css('background-color', '') - addState = 10 - }) - $('#version_change').click(function () { - if (bg_version == 1) { - bg_version = 2 - } else { - bg_version = 1 - } - // 类型改变 - $('#source_front').attr('src', './public/images/front_bg' + bg_version + '_2.png') - canvas.renderAll() - }) - $('#front_side').click(function () { - $(this).addClass('ui-button-active') - $('#back_side').removeClass('ui-button-active') - $('.canvas-container:eq(0)').show() - $('.canvas-container:eq(1)').hide() - console.log('------') - console.log(recordObjs1) - console.log('------') - canvas = canvas1 - objs = objs1 - background_image = background_image1 - next_objs = next_objs1 - pre_objs = pre_objs1 - next_json = next_json1 - pre_json = pre_json1 - recordObjs = recordObjs1 - recordJson = recordJson1 - step = step1 - updateList() - - $('#line_control').hide() - $('#base_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) //背景 - }) - $('#back_side').click(function () { - $(this).addClass('ui-button-active') - $('#front_side').removeClass('ui-button-active') - $('.canvas-container:eq(0)').hide() - $('.canvas-container:eq(1)').show() - canvas = canvas2 - objs = objs2 - background_image = background_image2 - next_objs = next_objs2 - pre_objs = pre_objs2 - next_json = next_json2 - pre_json = pre_json2 - recordObjs = recordObjs2 - recordJson = recordJson2 - step = step2 - updateList() - $('#line_control').hide() - $('#base_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) //"背景" - }) - $('#top_alignment').click(function () { - let _objs = canvas.getActiveObjects() - if (_objs.length == 1) { - //只选中了一个 - _objs[0].set('top', background_image.get('top')) - canvas.renderAll() - return - } - let top = 0 - for (let item of _objs) { - let t_top = item.get('top') - if (t_top < top) { - top = t_top - } - } - groupItems = [] - for (let item of _objs) { - item.set('top', top) - groupItems.push(item) - } - canvas.discardActiveObject() - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas - }) - canvas.setActiveObject(sel) - canvas.requestRenderAll() - canvas.renderAll() - }) - $('#bottom_alignment').click(function () { - let _objs = canvas.getActiveObjects() - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get('strokeWidth') - } - _objs[0].set('top', background_image.get('top') + background_image.get('height') - _objs[0].get('height') * _objs[0].get('scaleY') - other) - canvas.renderAll() - return - } - let bottom = 0 - for (let item of _objs) { - let idx = getIndex(item) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - let t_b = item.get('top') + item.get('height') * item.get('scaleY') + other - if (t_b > bottom) { - bottom = t_b - } - } - groupItems = [] - for (let item of _objs) { - let idx = getIndex(item) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - item.set('top', bottom - item.get('height') * item.get('scaleY') - other) - groupItems.push(item) - } - canvas.discardActiveObject() - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas - }) - canvas.setActiveObject(sel) - canvas.requestRenderAll() - canvas.renderAll() - }) - $('#center_y_alignment').click(function () { - let _objs = canvas.getActiveObjects() - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get('strokeWidth') - } - _objs[0].set('top', background_image.get('top') + (background_image.get('height') - (_objs[0].get('height') + other) * _objs[0].get('scaleY')) / 2) - canvas.renderAll() - return - } - let top = 0 - for (let item of _objs) { - let t_top = item.get('top') - if (t_top < top) { - top = t_top - } - } - groupItems = [] - for (let item of _objs) { - let idx = getIndex(item) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - item.set('top', top + (item.group.height - (item.get('height') + other) * item.get('scaleY')) / 2) - groupItems.push(item) - } - canvas.discardActiveObject() - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas - }) - canvas.setActiveObject(sel) - canvas.requestRenderAll() - canvas.renderAll() - }) - $('#left_alignment').click(function () { - let _objs = canvas.getActiveObjects() - if (_objs.length == 1) { - //只选中了一个 - _objs[0].set('left', background_image.get('left')) - canvas.renderAll() - return - } - let left = 0 - for (let item of _objs) { - let t_left = item.get('left') - if (t_left < left) { - left = t_left - } - } - groupItems = [] - for (let item of _objs) { - item.set('left', left) - groupItems.push(item) - } - canvas.discardActiveObject() - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas - }) - canvas.setActiveObject(sel) - canvas.requestRenderAll() - canvas.renderAll() - }) - $('#right_alignment').click(function () { - let _objs = canvas.getActiveObjects() - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - _objs[0].set('left', background_image.get('left') + background_image.get('width') - _objs[0].get('width') * _objs[0].get('scaleX') - other) - canvas.renderAll() - return - } - let right = 0 - for (let item of _objs) { - let idx = getIndex(item) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - let t_r = item.get('left') + item.get('width') * item.get('scaleX') + other - if (t_r > right) { - right = t_r - } - } - let groupItems = [] - - for (let item of _objs) { - let idx = getIndex(item) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - item.set('left', right - item.get('width') * item.get('scaleX') - other) - groupItems.push(item) - } - //BUG1 - canvas.discardActiveObject() - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas - }) - canvas.setActiveObject(sel) - canvas.requestRenderAll() - canvas.renderAll() - }) - $('#center_x_alignment').click(function () { - let _objs = canvas.getActiveObjects() - if (_objs.length == 1) { - //只选中了一个 - let idx = getIndex(_objs[0]) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = _objs[0].get('strokeWidth') - } - _objs[0].set('left', background_image.get('left') + (background_image.get('width') - (_objs[0].get('width') + other) * _objs[0].get('scaleX')) / 2) - canvas.renderAll() - return - } - let left = 0 - for (let item of _objs) { - let t_left = item.get('left') - if (t_left < left) { - left = t_left - } - } - groupItems = [] - for (let item of _objs) { - let idx = getIndex(item) - let type = objs[idx - 1].type - let other = 0 //strokeWidth - if (type == 5 || type == 6 || type == 7) { - other = item.get('strokeWidth') - } - item.set('left', left + (item.group.width - (item.get('width') + other) * item.get('scaleX')) / 2) - groupItems.push(item) - } - canvas.discardActiveObject() - let sel = new fabric.ActiveSelection(groupItems, { - canvas: canvas - }) - canvas.setActiveObject(sel) - canvas.requestRenderAll() - canvas.renderAll() - }) - $('#up_obj').click(function () { - //上移,放到前面 数组中在后面 - let idx = getIndex(canvas.getActiveObject()) - if (idx + 1 != canvas.getObjects().length) { - canvas.bringForward(canvas.getActiveObject()) - canvas.renderAll() - let temp = objs[idx - 1] - objs[idx - 1] = objs[idx] - objs[idx] = temp - //console.log(objs); - //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; - - recordState() - updateList() - selectObj(idx) - } - }) - $('#down_obj').click(function () { - //下移,放到后面 数组中在前面 - let idx = getIndex(canvas.getActiveObject()) - if (idx != 1) { - canvas.sendBackwards(canvas.getActiveObject()) - canvas.renderAll() - //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; - - let temp = objs[idx - 1] - objs[idx - 1] = objs[idx - 2] - objs[idx - 2] = temp - recordState() - updateList() - selectObj(idx - 2) - } - }) - $('#set_bg').click(function () { - //设置为背景 - let obj = canvas.getActiveObject() - let idx = getIndex(canvas.getActiveObject()) - const scaleX = 1012 / obj.width - const scaleY = 648 / obj.height - obj.set({ - scaleX: scaleX, - scaleY: scaleY, - left: ($('#canvas1').width() - 1012) / 2, - top: ($('#canvas1').height() - 648) / 2 - }) - $('#text_w').val(1012) - $('#text_h').val(648) - $('#text_x').val(0) - $('#text_y').val(0) - canvas.sendToBack(canvas.getActiveObject()) - canvas.bringForward(canvas.getActiveObject()) - canvas.renderAll() - let objItem = objs.splice(idx - 1, 1)[0] - objs.unshift(objItem) - recordState() - updateList() - selectObj(0) - }) - $('#previous').click(function () { - //撤销一次操作 - if (step.val == 0) { - return - } - - step.val-- - console.log(recordJson[step.val]) - canvas.loadFromJSON(recordJson[step.val]) - if ($('#front_side').hasClass('ui-button-active')) { - //objs = JSON.parse(recordObjs[step.val]); - objs1 = JSON.parse(recordObjs[step.val]) - objs = objs1 - } else if ($('#back_side').hasClass('ui-button-active')) { - //objs = JSON.parse(recordObjs[step.val]); - objs2 = JSON.parse(recordObjs[step.val]) - objs = objs2 - } - - background_image = canvas.getObjects()[0] - canvas.renderAll() - //recordState(); - updateList() - - selectObj(-1) - - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - }) - $('#next').click(function () { - //撤销一次撤销操作 - if (step.val >= recordObjs.length - 1) { - return - } - - ++step.val - canvas.loadFromJSON(recordJson[step.val]) - if ($('#front_side').hasClass('ui-button-active')) { - //objs = JSON.parse(recordObjs[step.val]); - objs1 = JSON.parse(recordObjs[step.val]) - objs = objs1 - } else if ($('#back_side').hasClass('ui-button-active')) { - //objs = JSON.parse(recordObjs[step.val]); - objs2 = JSON.parse(recordObjs[step.val]) - objs = objs2 - } - background_image = canvas.getObjects()[0] - canvas.renderAll() - selectObj(-1) - - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - updateList() - }) - $('#clean').click(function () { - objs.splice(0, objs.length) - - let allObjs = canvas.getObjects() - for (let item of allObjs) { - if (item != allObjs[0]) { - canvas.remove(item) - } - } - updateList() - recordState() - }) - $('#rotate').click(function () { - let obj = canvas.getActiveObject() - obj.rotate(Math.round((obj.get('angle') + 90) % 360)) - $('#text_r').val(obj.get('angle')) - canvas.renderAll() - updateControls() - recordState() - }) - $('#text_italics').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选 - canvas.getActiveObject().set('fontStyle', 'normal') - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('fontStyle', 'italic') - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#text_bold').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选,恢复正常 - canvas.getActiveObject().set('fontWeight', 'normal') - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('fontWeight', 'bold') - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#text_underline').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选 - canvas.getActiveObject().set('underline', false) - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('underline', true) - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#text_linethrough').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选 - canvas.getActiveObject().set('linethrough', false) - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('linethrough', true) - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $("#circle_text_italics").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选 - canvas.getActiveObject().set("fontStyle", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontStyle", "italic") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $("#circle_text_bold").click(function () { - if ($(this).hasClass("ui-radio-acitve")) { - //已选,恢复正常 - canvas.getActiveObject().set("fontWeight", "normal") - $(this).removeClass("ui-radio-acitve") - } else { - //未选 - canvas.getActiveObject().set("fontWeight", "bold") - $(this).addClass("ui-radio-acitve") - } - recordState(); - canvas.renderAll(); - }); - $('#out_left_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_left_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'left') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'left' - recordState() - canvas.renderAll() - }) - $('#out_right_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_right_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'right') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'right' - recordState() - canvas.renderAll() - }) - $('#out_center_alignment').click(function () { - $('#out_left_alignment').parent().removeClass('tool1') - $('#out_right_alignment').parent().removeClass('tool1') - $('#out_center_alignment').parent().removeClass('tool1') - setTimeout(() => { - $('#out_center_alignment').parent().addClass('tool1') - }); - canvas.getActiveObject().set('textAlign', 'center') - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].align = 'center' - recordState() - canvas.renderAll() - }) - $('#out_text_italics').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选 - canvas.getActiveObject().set('fontStyle', 'normal') - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('fontStyle', 'italic') - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#out_text_bold').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选,恢复正常 - canvas.getActiveObject().set('fontWeight', 'normal') - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('fontWeight', 'bold') - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#out_text_underline').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选 - canvas.getActiveObject().set('underline', false) - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('underline', true) - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#out_text_linethrough').click(function () { - if ($(this).hasClass('ui-radio-acitve')) { - //已选 - canvas.getActiveObject().set('linethrough', false) - $(this).removeClass('ui-radio-acitve') - } else { - //未选 - canvas.getActiveObject().set('linethrough', true) - $(this).addClass('ui-radio-acitve') - } - recordState() - canvas.renderAll() - }) - $('#lock').click(function () { - canvas.getActiveObject().set('selectable', !canvas.getActiveObject().get('selectable')) - if (canvas.getActiveObject().get('selectable')) { - $('#lock_img').attr('src', 'public/images/unlock20.png') //已经上锁,需要解锁 - } else { - $('#lock_img').attr('src', 'public/images/lock20.png') //没上锁,可以上锁 - } - recordState() - canvas.renderAll() - }) - $('#text_back_transparent').click(function () { - canvas.getActiveObject().set('textBackgroundColor', '') - $('#text_back_color').val('#ffffff') - canvas.renderAll() - }) - $('#out_text_back_transparent').click(function () { - canvas.getActiveObject().set('textBackgroundColor', '') - $('#out_text_back_color').val('#ffffff') - canvas.renderAll() - }) - $('#rect_transparent').click(function () { - canvas.getActiveObject().set('fill', '') - $('#rect_color').val('#ffffff') - canvas.renderAll() - }) - $('#rect_stroke_transparent').click(function () { - canvas.getActiveObject().set('stroke', '') - $('#rect_stroke').val('#ffffff') - canvas.renderAll() - }) - $('#circle_transparent').click(function () { - canvas.getActiveObject().set('fill', '') - $('#circle_color').val('#ffffff') - canvas.renderAll() - }) - $('#circle_stroke_transparent').click(function () { - canvas.getActiveObject().set('stroke', '') - $('#circle_stroke').val('#ffffff') - canvas.renderAll() - }) - function saveAs(op1, callback) { - let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let _g1 = [], - _g2 = [] - let left = 0, - top = 0 - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g1.push(fabric.util.object.clone(_o)) - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - ; (left = 0), (top = 0) - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g2.push(fabric.util.object.clone(_o)) - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } - let con_o = Object.assign(o, op1) - dialog - .showSaveDialog({ - title: language_str('saveFile'), //'保存文件' - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }) - .then((result) => { - if (result.filePath == '') { - return - } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon' - } - let fs = require('fs') - con_o.soonType = 2 - fs.writeFileSync(result.filePath, JSON.stringify(con_o)) - openAs.name = result.filePath - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - saveHistory() - if (callback && typeof callback === 'function') { - callback() - } - }) - .catch((err) => { - console.log() - }) - } - function save(op1, callback) { - let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let _g1 = [], - _g2 = [] - let left = 0, - top = 0 - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g1.push(fabric.util.object.clone(_o)) - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - ; (left = 0), (top = 0) - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g2.push(fabric.util.object.clone(_o)) - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } - let con_o = Object.assign(o, op1) - console.log(op1) - if (openAs.name != '') { - //打开的文件 - let fs = require('fs') - con_o.soonType = 2 - fs.writeFileSync(openAs.name, JSON.stringify(con_o)) - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - saveHistory() - if (callback && typeof callback === 'function') { - callback() - } - return - } - dialog - .showSaveDialog({ - title: language_str('saveFile'), //'保存文件' - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }) - .then((result) => { - if (result.filePath == '') { - return - } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon' - } - let fs = require('fs') - con_o.soonType = 2 - fs.writeFileSync(result.filePath, JSON.stringify(con_o)) - openAs.name = result.filePath - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - saveHistory() - if (callback && typeof callback === 'function') { - callback() - } - }) - .catch((err) => { - console.log() - }) - } - // 复制 - $("#copy").click(function () { - copySelection(); - }) - // 粘贴 - $("#paste").click(function () { - pasteFromClipboard(); - }) - // 保存 - $('#save').click(function () { - output() - return - let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let _g1 = [], - _g2 = [] - let left = 0, - top = 0 - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g1.push(fabric.util.object.clone(_o)) - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - ; (left = 0), (top = 0) - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g2.push(fabric.util.object.clone(_o)) - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } - let con_o = Object.assign(o, op1) - console.log(op1) - if (openAs.name != '') { - //打开的文件 - let fs = require('fs') - con_o.soonType = 2 - fs.writeFileSync(openAs.name, JSON.stringify(con_o)) - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - saveHistory() - return - } - dialog - .showSaveDialog({ - title: language_str('saveFile'), //'保存文件' - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }) - .then((result) => { - if (result.filePath == '') { - return - } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon' - } - let fs = require('fs') - con_o.soonType = 2 - fs.writeFileSync(result.filePath, JSON.stringify(con_o)) - openAs.name = result.filePath - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - saveHistory() - }) - .catch((err) => { - console.log() - }) - }) - function getDate() { - let date = new Date() - return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() - } - function getAbsoluteXY(_obj) { - let coord = _obj.get('lineCoords') - //let xs = [], ys = []; - //for(let item in coord){ - // xs.push(coord[item].x); - // ys.push(coord[item].y); - //} - //xs.sort(function(a,b){return a-b}); - //ys.sort(function(a,b){return a-b}); - //return [xs[0], ys[0]]; - return [_obj.get('left'), _obj.get('top')] - } - let fullPath = path.join(exePath, 'data.json') - function saveHistory() { - let fs = require('fs') - let j = {} - try { - fsData = fs.readFileSync(fullPath) - j = JSON.parse(fsData.toString()) - for (let item of j.history) { - if (item.path == openAs.name) { - item.time = getDate() - fs.writeFileSync(fullPath, JSON.stringify(j)) - return - } - } - j.history.push({ time: getDate(), path: openAs.name }) - fs.writeFileSync(fullPath, JSON.stringify(j)) - } catch (e) { - let j = { history: { time: getDate(), path: openAs.name } } - fs.writeFileSync(fullPath, JSON.stringify(j)) - } - } - function open(file) { - let fs = require('fs') - var fsData = fs.readFileSync(file) - openAs.name = file - let j = JSON.parse(fsData.toString()) - let left = background_image.left - let top = background_image.top - let new_left = j.f.objects[0].left - let new_top = j.f.objects[0].top - for (let o of j.f.objects) { - o.left = o.left - new_left + left - o.top = o.top - new_top + top - } - for (let o of j.b.objects) { - o.left = o.left - new_left + left - o.top = o.top - new_top + top - } - canvas1.loadFromJSON(j.f) - canvas2.loadFromJSON(j.b) - background_image1 = canvas1.getObjects()[0] - background_image2 = canvas2.getObjects()[0] - - objs2 = j.bo - objs1 = j.fo - step1.val = 0 - step2.val = 0 - recordJson1 = [] - recordJson2 = [] - recordObjs1 = [] - recordObjs2 = [] - if ($('#front_side').hasClass('ui-button-active')) { - objs = objs1 - recordObjs = recordObjs1 - recordJson = recordJson1 - step = step1 - background_image = background_image1 - } else if ($('#back_side').hasClass('ui-button-active')) { - objs = objs2 - recordObjs = recordObjs2 - recordJson = recordJson2 - background_image = background_image2 - step = step2 - } - canvas1.renderAll() - canvas2.renderAll() - updateList() /* - 修复打开新文件后无法撤销的bug - 2022-07-16 - */ - setTimeout(() => { - recordObjs1.push(JSON.stringify(objs1)) - let j1 = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - j1.objects[0].hoverCursor = 'default' - recordJson1.push(j1) - - recordObjs2.push(JSON.stringify(objs2)) - let j2 = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - j2.objects[0].hoverCursor = 'default' - recordJson2.push(j2) - }, 0) - } - $('#open').click(function () { - if (step.val == 0) { - OpenDialog(); - return; - } - layer.confirm( - '' + language_str('whetherSave') + '', - function (index) { - //confirm 是否保存当前文件? - //let j = canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); - //let b = canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); - //let o = { f : j, b : b,fo : objs1, bo : objs2}; - //if(openAs != ""){ - // //打开的文件 - // let fs = require('fs') - // fs.writeFileSync(openAs,JSON.stringify(o)) - // layer.msg('保存成功至'+ openAs); - // - // OpenDialog();return; - //} - //dialog.showSaveDialog({ - // title:'保存文件', - // filters: [ - // { name: 'FCS File Type', extensions: ['fcs'] }, - // ] - //}).then(result=>{ - // let fs = require('fs') - // fs.writeFileSync(result.filePath,JSON.stringify(o)) - // - // OpenDialog(); - //}).catch(err=>{ - // console.log() - //}) - /* - let j = canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); - let b = canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); - let _g1 = [], _g2 = []; - let left = 0, top = 0; - for( let _o of canvas1.getObjects() ){ - if( getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left){ - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if( getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top){ - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g1.push(fabric.util.object.clone(_o)); - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 648, - width: 1012, - top:top/zoom, - left:left/zoom - }); - left = 0, top = 0; - for( let _o of canvas2.getObjects() ){ - if( getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left){ - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); - } - if( getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top){ - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); - } - _g2.push(fabric.util.object.clone(_o)); - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 648, - width: 1012, - top:top/zoom, - left:left/zoom - }); - let o = { f : j, b : b,fo : objs1, bo : objs2,fp:fp,bp:bp}; - if(openAs.name != ""){ - //打开的文件 - let fs = require('fs') - fs.writeFileSync(openAs.name,JSON.stringify(o)) - layer.msg(language_str("saveSucc")+ openAs.name);//'保存成功至' - saveHistory(); - - OpenDialog(); - return; - } - dialog.showSaveDialog({ - title:language_str("saveFile"),//'保存文件' - filters: [ - { name: 'Soon File Type', extensions: ['soon'] }, - ] - }).then(result=>{ - if(result.canceled)return; - if(result.filePath.substring(result.filePath.length-5).indexOf('.') == -1){ - result.filePath += '.soon'; - } - let fs = require('fs') - fs.writeFileSync(result.filePath,JSON.stringify(o)) - openAs.name = result.filePath; - layer.msg(language_str("saveSucc")+ openAs.name);//'保存成功至' - saveHistory(); - OpenDialog(); - }).catch(err=>{ - console.log() - }) - */ - output(OpenDialog) - layer.close(index) - }, - function (index) { - OpenDialog() - } - ) - function OpenDialog() { - dialog - .showOpenDialog({ - title: '请选择文件', //confirm - buttonLabel: language_str('comf'), //"确认" - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }) - .then((result) => { - if (result.canceled) return - //console.log(result.filePaths) - let fs = require('fs') - var fsData = fs.readFileSync(result.filePaths[0]) - openAs.name = result.filePaths[0] - let j = JSON.parse(fsData.toString()) - let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; - if (type == 1) { - ipcRenderer.send('open-design-page', openAs.name, type); - return - } - let left = background_image.left - let top = background_image.top - let new_left = j.f.objects[0].left - let new_top = j.f.objects[0].top - for (let o of j.f.objects) { - o.left = o.left - new_left + left - o.top = o.top - new_top + top - } - for (let o of j.b.objects) { - o.left = o.left - new_left + left - o.top = o.top - new_top + top - } - canvas1.loadFromJSON(j.f) - canvas2.loadFromJSON(j.b) - background_image1 = canvas1.getObjects()[0] - background_image2 = canvas2.getObjects()[0] - - objs2 = j.bo - objs1 = j.fo - step1.val = 0 - step2.val = 0 - recordJson1 = [] - recordJson2 = [] - recordObjs1 = [] - recordObjs2 = [] - if ($('#front_side').hasClass('ui-button-active')) { - objs = objs1 - recordObjs = recordObjs1 - recordJson = recordJson1 - step = step1 - background_image = background_image1 - } else if ($('#back_side').hasClass('ui-button-active')) { - objs = objs2 - recordObjs = recordObjs2 - recordJson = recordJson2 - background_image = background_image2 - step = step2 - } - canvas1.renderAll() - canvas2.renderAll() - updateList() - /* - 修复打开新文件后无法撤销的bug - 2022-07-16 - */ - setTimeout(() => { - recordObjs1.push(JSON.stringify(objs1)) - let j1 = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - j1.objects[0].hoverCursor = 'default' - recordJson1.push(j1) - - recordObjs2.push(JSON.stringify(objs2)) - let j2 = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - j2.objects[0].hoverCursor = 'default' - recordJson2.push(j2) - }, 0) - }) - .catch((err) => { - console.log(err) - }) - } - }) - function clearAll() { - //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; - //let j = JSON.parse(blank); - //canvas1.setZoom(1); - //canvas2.setZoom(1); - //canvas1.loadFromJSON(j.f); - //canvas2.loadFromJSON(j.b); - let t_objs1 = canvas1.getObjects() - let t_objs2 = canvas2.getObjects() - for (let i = 1; i < t_objs1.length; i++) { - canvas1.remove(t_objs1[i]) - } - for (let i = 1; i < t_objs2.length; i++) { - canvas2.remove(t_objs2[i]) - } - objs2 = [] - objs1 = [] - step1.val = 0 - step2.val = 0 - recordJson1 = [] - recordJson2 = [] - recordObjs1 = [] - recordObjs2 = [] - if ($('#front_side').hasClass('ui-button-active')) { - objs = objs1 - recordObjs = recordObjs1 - recordJson = recordJson1 - step = step1 - } else if ($('#back_side').hasClass('ui-button-active')) { - objs = objs2 - recordObjs = recordObjs2 - recordJson = recordJson2 - step = step2 - } - openAs.name = '' - canvas1.renderAll() - canvas2.renderAll() - updateList() - console.log() - } - $('#new').click(function () { - if (step.val == 0) { - ipcRenderer.send('open-first-page'); - return; - } - layer.confirm( - '' + language_str('whetherSave') + '', - function (index) { - //confirm 是否保存当前文件? - output(() => ipcRenderer.send('open-first-page')) - return - let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - let _g1 = [], - _g2 = [] - let left = 0, - top = 0 - for (let _o of canvas1.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g1.push(fabric.util.object.clone(_o)) - } - let fp = new fabric.Group(_g1).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - ; (left = 0), (top = 0) - for (let _o of canvas2.getObjects()) { - if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { - left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) - } - if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { - top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) - } - _g2.push(fabric.util.object.clone(_o)) - } - //let fp = new fabric.Group(_g1).toDataURL(); - let bp = new fabric.Group(_g2).toDataURL({ - height: 648, - width: 1012, - top: top / zoom, - left: left / zoom - }) - let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } - if (openAs.name != '') { - //打开的文件 - let fs = require('fs') - o.soonType = 2 - fs.writeFileSync(openAs.name, JSON.stringify(o)) - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - /* - 2022-07-16 直接跳转至首页 - */ - ipcRenderer.send('open-first-page') - //saveHistory(); - //clearAll(); - return - } - dialog - .showSaveDialog({ - title: language_str('saveFile'), //'保存文件' - filters: [{ name: 'Soon File Type', extensions: ['soon'] }] - }) - .then((result) => { - if (result.filePath == '') { - return - } - if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { - result.filePath += '.soon' - } - let fs = require('fs') - o.soonType = 2 - fs.writeFileSync(result.filePath, JSON.stringify(o)) - openAs.name = result.filePath - layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' - /* - 2022-07-16 直接跳转至首页 - */ - ipcRenderer.send('open-first-page') - //saveHistory(); - //clearAll(); - }) - .catch((err) => { - console.log() - }) - layer.close(index) - }, - function (index) { - /* - 2022-07-16 直接跳转至首页 - */ - //clearAll(); - ipcRenderer.send('open-first-page') - } - ) - }) - function chagneTitle() { } - function addBackground() { - // 类型改变 - fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { - //image.set("scaleX",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) - //image.set("scaleY",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) - - image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 - image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 - - image.selectable = false - image.hoverable = false - image.hoverCursor = 'default' - image.setSrc(image.toDataURL(), function (image) { - background_image1 = image - background_image = background_image1 - canvas.add(image) - canvas.renderAll() - //pre_objs1.push(JSON.stringify(objs1)); - //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); - - recordObjs1.push(JSON.stringify(objs1)) - recordJson1.push(canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) - }) - // 类型改变 - fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { - image.left = ($('#canvas2').width() - image.width) / 2 - image.top = ($('#canvas2').height() - image.height) / 2 - - image.selectable = false - image.hoverable = false - image.hoverCursor = 'default' - - image.setSrc(image.toDataURL(), function (image) { - background_image2 = image - canvas2.add(image) - canvas2.renderAll() - //pre_objs2.push(JSON.stringify(objs2)); - //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); - - recordObjs2.push(JSON.stringify(objs2)) - recordJson2.push(canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) - - let file = GetFile().get('file') - console.log(file) - if (file && file != 'empty') { - open(file) - } - }) - }) - is_bgi_add = true - }) - } - function selectObject(target, index) { - updateControls() - - $('#base_control').show() - //$("#text_x").val((target.left - background_image.left).toFixed(1)); - //$("#text_y").val((target.top - background_image.top).toFixed(1)); - //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); - //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); - //$("#text_r").val(target.get("angle").toFixed(0)); - } - function updateControls() { - $('#text_r').val(canvas.getActiveObject().get('angle').toFixed(0)) - $('#text_x').val((canvas.getActiveObject().get('left') - background_image.left).toFixed(0)) - $('#text_y').val((canvas.getActiveObject().get('top') - background_image.top).toFixed(0)) - $('#text_h').val((canvas.getActiveObject().get('scaleY') * canvas.getActiveObject().get('height')).toFixed(0)) - $('#text_w').val((canvas.getActiveObject().get('scaleX') * canvas.getActiveObject().get('width')).toFixed(0)) - if (canvas.getActiveObject().get('selectable')) { - $('#lock_img').attr('src', 'public/images/unlock20.png') //已经上锁,需要解锁 - } else { - $('#lock_img').attr('src', 'public/images/lock20.png') //没上锁,可以上锁 - } - } - function checkName(name) { - //检查字段名是否重复了 - for (let item of objs1) { - if (item.name == name) { - return false - } - } - for (let item of objs2) { - if (item.name == name) { - return false - } - } - return true - } - function resName(pre_name) { - //返回的是不重复的字段名 - let num = objs1.length + objs2.length - 1 - do { - num++ - } while (!checkName(pre_name + num)) //重复了就再加1 - return pre_name + num - } - function addPic(pointer) { - pre_add_image.left = pointer.x - pre_add_image.top = pointer.y - - canvas.add(pre_add_image) - - objs.push({ type: 1, black: false }) - canvas.renderAll() - updateList() - recordState() - //OpenDialog(); - //function OpenDialog() - //{ - // dialog.showOpenDialog({ - // title: "请选择图片", - // buttonLabel: "确认", - // filters: [ - // { name: 'Image File Type', extensions: ['png', 'jpg'] }, - // ] - // }).then(result => { - // if(result.canceled)return; - // //console.log(result.filePaths) - // fabric.Image.fromURL(result.filePaths[0], function(image) { - // image.left = pointer.x; - // image.top = pointer.y; - // - // canvas.add(image); - // objs.push({type: 1}); - // canvas.renderAll(); - // updateList(); - // recordState(); - // } ); - // }).catch(err => { - // console.log(err) - // }) - //} - } - function selectPic(target, index) { - //透明度 - - $('#pic_control').show() - $("#set_bg").show(); - pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) - - let idx = getIndex(canvas.getActiveObject()) - // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); - - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#line_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function addOutPic(pointer) { - fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { - let width = canvas.getWidth() - let height = canvas.getHeight() - image.left = pointer.x - image.top = pointer.y - image.opacity = 1 - //image.on("selected",function(){//test well - // console.log(222); - //}); - image.setSrc(image.toDataURL(), function (image) { - canvas.add(image) - objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) - canvas.renderAll() - updateList() - recordState() - }) - }) - } - function selectOutPic(target, index) { - //字段名 - $('#out_pic_control').show() - out_pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) - let idx = getIndex(canvas.getActiveObject()) - $('#out_pic_field_name').val(objs[idx - 1].name) - // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); - - $('#pic_control').hide() - $("#set_bg").hide(); - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#line_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - - function addText(pointer) { - let text = new fabric.IText('TEXT', { - //源:文本 - left: pointer.x, - top: pointer.y, - fill: '#000000', - textBackgroundColor: '', - fontStyle: 'normal', - fontWeight: 'normal', - underline: false, - linethrough: false, - fontFamily: 'Arial' - }) - text.setControlVisible('ml', false) - text.setControlVisible('mt', false) - text.setControlVisible('mr', false) - text.setControlVisible('mb', false) - canvas.add(text) - objs.push({ type: 3, black: false }) - canvas.renderAll() - updateList() - recordState() - } - function selectText() { - //console.log(canvas.getActiveObject().get("fontFamily")); - $('#text_control').show() - $('#text_color').val(canvas.getActiveObject().get('fill')) - $('#text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) - let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#text_size").val(val); - $('#text_text').val(canvas.getActiveObject().get('text')) - $('#text_font_family').val(canvas.getActiveObject().get('fontFamily')) - - if (canvas.getActiveObject().get('underline') == true) { - $('#text_underline').addClass('ui-radio-acitve') - } else { - $('#text_underline').removeClass('ui-radio-acitve') - } - if (canvas.getActiveObject().get('fontWeight') == 'bold') { - $('#text_bold').addClass('ui-radio-acitve') - } else { - $('#text_bold').removeClass('ui-radio-acitve') - } - if (canvas.getActiveObject().get('fontStyle') == 'italic') { - $('#text_italics').addClass('ui-radio-acitve') - } else { - $('#text_italics').removeClass('ui-radio-acitve') - } - if (canvas.getActiveObject().get('linethrough') == true) { - $('#text_linethrough').addClass('ui-radio-acitve') - } else { - $('#text_linethrough').removeClass('ui-radio-acitve') - } - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#line_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function getDisdance(x1, y1, x2, y2) { - var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 - var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 - var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 - return distance; - } - function getDeg(pointer) { - // 计算点击位置与画布中心的坐标差 - var centerX = canvas.width / 2; - var centerY = canvas.height / 2; - var mouseX = pointer.x; - var mouseY = pointer.y; - var dx = mouseX - centerX; - var dy = mouseY - centerY; - // 计算旋转角度(弧度) - var angleRad = Math.atan2(dy, dx); - // 将角度转换为度数 - var angleDeg = angleRad * (180/Math.PI); - if (angleDeg < 0) { - angleDeg += 360; - } - angleDeg += 90 - return angleDeg - } - function addCircleText(pointer) { - let distance = getDisdance(pointer.x, pointer.y, canvas.width/2,canvas.height/2) - let angleDeg = getDeg(pointer) - let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 - left: pointer.x, - top: pointer.y, - fill: "#000000", - textBackgroundColor: "", - fontStyle: "normal", - fontWeight: "normal", - diameter: distance*2, - fontSize: 30, - underline: false, - linethrough: false, - lockScalingX: true, - lockScalingY: true, - fontFamily: 'Arial', - originX: 'center', - originY: 'center', - angle: angleDeg, - flipped: pointer.y > canvas.height/2 ? true : false - }); - // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) - // text.set({diameter: distance*2}) - text.setControlVisible("ml", false); - text.setControlVisible("mt", false); - text.setControlVisible("mr", false); - text.setControlVisible("mb", false); - canvas.add(text); - objs.push({ type: 11, black: false, circleCenter: 0 }); - canvas.renderAll(); - updateList(); - recordState(); - } - function selectCircleText() { - console.log(canvas.getActiveObject().get("text")) - let idx = getIndex(canvas.getActiveObject()); - $("#circle_text_control").show(); - $("#circle_text_color").val(canvas.getActiveObject().get("fill")); - $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); - circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); - circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")-50); - $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); - $("#circle_text_text").val(canvas.getActiveObject().get("text")); - $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); - - if (canvas.getActiveObject().get("underline") == true) { - $("#circle_text_underline").addClass("ui-radio-acitve") - } else { - $("#circle_text_underline").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontWeight") == "bold") { - $("#circle_text_bold").addClass("ui-radio-acitve") - } else { - $("#circle_text_bold").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("fontStyle") == "italic") { - $("#circle_text_italics").addClass("ui-radio-acitve") - } else { - $("#circle_text_italics").removeClass("ui-radio-acitve") - } - if (canvas.getActiveObject().get("linethrough") == true) { - $("#circle_text_linethrough").addClass("ui-radio-acitve") - } else { - $("#circle_text_linethrough").removeClass("ui-radio-acitve") - } - $("#pic_control").hide(); - $("#set_bg").hide(); - $("#rotate").hide(); - $("#out_pic_control").hide(); - $("#text_control").hide(); - $("#out_text_control").hide(); - $("#rect_control").hide(); - $("#circle_control").hide(); - $("#line_control").hide(); - $("#counter_control").hide(); - $("#qrcode_Controll").hide(); - $("#barcode_controll").hide(); - - } - function addOutText(pointer) { - //MERGE_TEXT_1 - let text = new fabric.Text('MERGE TEXT', { - //源:合成文本 - left: pointer.x, - top: pointer.y, - fill: '#000000', - fontStyle: 'normal', - fontWeight: 'normal', - textBackgroundColor: '', - underline: false, - linethrough: false, - fontFamily: 'Arial', - textAlign: 'center' - }) - - //text.on({ - // 'scaling': function(e) { - // var obj = this, - // w = obj.width * obj.scaleX, - // h = obj.height * obj.scaleY; - // - // obj.set({ - // 'height' : h, - // 'width' : w, - // 'scaleX' : 1, - // 'scaleY' : 1 - // }); - // } - //}); - text.setControlVisible('ml', false) - text.setControlVisible('mt', false) - text.setControlVisible('mr', false) - text.setControlVisible('mb', false) - - canvas.add(text) - objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) - canvas.renderAll() - updateList() - recordState() - } - function selectOutText() { - //color - //size - const align = canvas.getActiveObject().get('textAlign') - if (align === 'left') { - $('#out_left_alignment').parent().addClass('tool1') - } else if (align === 'right') { - $('#out_right_alignment').parent().addClass('tool1') - } else { - $('#out_center_alignment').parent().addClass('tool1') - } - $('#out_text_control').show() - $('#out_text_color').val(canvas.getActiveObject().get('fill')) - $('#out_text_text').val(canvas.getActiveObject().get('text')) - let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#out_text_size").val(val); - $('#out_text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) - let idx = getIndex(canvas.getActiveObject()) - $('#out_text_field_name').val(objs[idx - 1].name) - $('#out_text_font_family').val(canvas.getActiveObject().get('fontFamily')) - $('#exceed').prop('checked', objs[idx - 1].exceed ? 'checked' : '') - $('#autoWrap').prop('checked', objs[idx - 1].autoWrap ? 'checked' : '') - $('#fixSize').prop('checked', objs[idx - 1].fixSize ? 'checked' : '') - if (canvas.getActiveObject().get('underline') == true) { - $('#out_text_underline').addClass('ui-radio-acitve') - } else { - $('#out_text_underline').removeClass('ui-radio-acitve') - } - if (canvas.getActiveObject().get('fontWeight') == 'bold') { - $('#out_text_bold').addClass('ui-radio-acitve') - } else { - $('#out_text_bold').removeClass('ui-radio-acitve') - } - if (canvas.getActiveObject().get('fontStyle') == 'italic') { - $('#out_text_italics').addClass('ui-radio-acitve') - } else { - $('#out_text_italics').removeClass('ui-radio-acitve') - } - if (canvas.getActiveObject().get('linethrough') == true) { - $('#out_text_linethrough').addClass('ui-radio-acitve') - } else { - $('#out_text_linethrough').removeClass('ui-radio-acitve') - } - - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#rect_control').hide() - $('#circle_control').hide() - $('#line_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function addRect(pointer) { - let rect = new fabric.Rect({ - left: pointer.x, - top: pointer.y, - fill: '', - stroke: "#ff0000", - strokeWidth: 3, - width: 100, - height: 100 - }) - canvas.add(rect) - objs.push({ type: 5, black: false }) - canvas.renderAll() - updateList() - recordState() - } - function selectRect() { - //color - $('#rect_control').show() - $('#rect_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) - $('#rect_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) - $('#rect_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) - $('#rect_radius').val(canvas.getActiveObject().get('rx')) - rect_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) - let a = canvas.getActiveObject().getCoords() - console.log(getCoordsMinX(a)) - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#circle_control').hide() - $('#line_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function addCircle(pointer) { - let circle = new fabric.Circle({ - left: pointer.x, - top: pointer.y, - fill: '', - stroke: '#ff0000', - strokeWidth: 3, - radius: 100, - strokeUniform: true - }) - canvas.add(circle) - objs.push({ type: 6, black: false }) - canvas.renderAll() - updateList() - recordState() - } - function selectCircle() { - //border-color - //color - $('#circle_control').show() - $('#circle_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) - $('#circle_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) - $('#circle_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) - circle_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#line_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function addLine(pointer) { - let line = new fabric.Line([50, 100, 200, 100], { - left: pointer.x, - top: pointer.y, - stroke: '#ff0000', - strokeWidth: 3, - strokeDashArray: [0, 0], - }) - - line.setControlVisible('tl', false) - line.setControlVisible('tr', false) - line.setControlVisible('br', false) - line.setControlVisible('bl', false) - line.setControlVisible('mt', false) - line.setControlVisible('mb', false) - canvas.add(line) - objs.push({ type: 7, black: false }) - canvas.renderAll() - updateList() - recordState() - } - function selectLine() { - //color - //weight - $('#line_control').show() - - let idx = getIndex(canvas.getActiveObject()) - $('#line_color').val(canvas.getActiveObject().get('stroke')) - $('#line_dist').val(canvas.getActiveObject().get('strokeDashArray')[0]) - $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) - - $('#pic_control').hide() - $("#set_bg").hide(); - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function addQrCode(pointer) { - let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) - fabric.Image.fromURL(qr, function (image) { - image.left = pointer.x - image.top = pointer.y - canvas.add(image) - objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) - canvas.renderAll() - updateList() - recordState() - }) - } - function selectQrCode() { - //字段名 - //background-color - //color - $('#qrcode_Controll').show() - let idx = getIndex(canvas.getActiveObject()) - $('#qrcode_field_name').val(objs[idx - 1].name) - $('#qrcode_color').val(objs[idx - 1].color) - $('#qrcode_val').val(objs[idx - 1].val) - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#barcode_controll').hide() - } - function addBarCode(pointer) { - JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) - let barcode = document.getElementById('barcode') - let bar = barcode.toDataURL('image/png') - fabric.Image.fromURL(bar, function (image) { - image.left = pointer.x - image.top = pointer.y - - canvas.add(image) - objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) //show是取消文字显示 - canvas.renderAll() - updateList() - recordState() - }) - } - function selectBarCode() { - //字段名 - //background-color - //color - $('#barcode_controll').show() - let idx = getIndex(canvas.getActiveObject()) - $('#barcode_field_name').val(objs[idx - 1].name) - $('#barcode_font_family').val(objs[idx - 1].font) - let val = parseFloat(objs[idx - 1].fontSize)*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#barcode_size").val(val); - $('#barcode_val').val(objs[idx - 1].val) - $('#barcode_show').prop('checked', objs[idx - 1].show ? 'checked' : '') - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - } - function addCounter(pointer) { - let text = new fabric.Text('0', { - left: pointer.x, - top: pointer.y, - fill: '#000000' - }) - canvas.add(text) - objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) - canvas.renderAll() - updateList() - recordState() - console.log(canvas.getObjects()) - } - function selectCounter() { - //起始 - //终止 - //增量 - $('#counter_control').show() - - let idx = getIndex(canvas.getActiveObject()) - $('#counter_start').val(objs[idx - 1].start) - $('#counter_add').val(objs[idx - 1].add) - $('#counter_color').val(canvas.getActiveObject().get('fill')) - $('#count_font_family').val(canvas.getActiveObject().get('fontFamily')) - let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi - if (val%0.5!==0) val = Math.round(val*2)/2 - $("#count_size").val(val); - - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - } - function getIndex(target, _canvas = null) { - let temp_objs - if (_canvas == null) { - temp_objs = canvas.getObjects() - } else { - temp_objs = _canvas.getObjects() - } - - let i = 0 - for (let res of temp_objs) { - if (target == res) { - return i - } - i++ - } - return 0 - } - function getType(target) { - let temp_objs = canvas.getObjects() - let i = 0 - for (let res of temp_objs) { - if (target == res) { - return objs[i].type - } - i++ - } - } - - function updateList() { - let str = '' - let i = 0 - for (let item of objs) { - switch (item.type) { - case 1 /* 2022-06-14修改*/: - str = '
IMG_" + i + '
' + str - break - case 2: - str = '
" + item.name + '
' + str - break - case 3: - str = '
" + 'TEXT_' + i + '
' + str - break - case 4: - str = '
" + item.name + '
' + str - break - case 5: - str = '
RECT_" + i + '
' + str - break - case 6: - str = '
CIRCLE_" + i + '
' + str - break - case 7: - str = '
LINE_" + i + '
' + str - break - case 8: - str = '
QRCODE_" + i + '
' + str - break - case 9: - str = '
BARCODE_" + i + '
' + str - break - case 10: - str = '
COUNTER_" + i + '
' + str - break - case 11: - str = "
" + "CIRCLE_TEXT_" + i + "
" + str; - break; - } - i++ - } - $('.obj_list').html(str) - } - function selectObj(i, notSelect = false) { - //for(let item in $(".obj_list").children()){ - // if(_i == i){ - // item.css("background-color","#6F7A84"); - // }else{ - // item.css("background-color",""); - // } - // _i++; - //} - $('.obj_list div').each(function (index) { - //if(index == i){ - if ($(this).children('img').attr('id') == 'obj' + i) { - $(this).css('background-color', '#6F7A84') - canvas.setActiveObject(canvas.getObjects()[i + 1]) - canvas.renderAll() - } else { - $(this).css('background-color', '') - } - }) - if (!notSelect) { - handleObjectSelected() - } - } - function recordState() { - //pre_objs.push(JSON.stringify(objs)); - //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - //next_json = []; - //next_objs = []; - //recordObjs.push(JSON.stringify(objs)); - //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); - ++step.val - for (let i = step.val; i < recordObjs.length; i++) { - recordObjs.pop() - recordJson.pop() - } - //recordObjs.splice(step.val, 1, JSON.stringify(objs)) - //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) - recordObjs.push(JSON.stringify(objs)) - j = canvas.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) - j.objects[0].hoverCursor = 'default' - recordJson.push(j) - console.log(recordJson) - } - function getCoordsMinX(acoords) { - x = acoords[0].x - for (let item of acoords) { - if (item.x < x) { - x = item.x - } - } - return x - } - function getCoordsMaxX(acoords) { - x = acoords[0].x - for (let item of acoords) { - if (item.x > x) { - x = item.x - } - } - return x - } - function getCoordsMinY(acoords) { - y = acoords[0].y - for (let item of acoords) { - if (item.y < y) { - y = item.y - } - } - return y - } - function getCoordsMaxY(acoords) { - y = acoords[0].y - for (let item of acoords) { - if (item.y > y) { - y = item.y - } - } - return y - } - - $('#text_y').bind('input propertychange', function () { - if ($('#text_y').val() == '') { - $('#text_y').val(0) - } - let val = background_image.top - if ($('#text_y').val() != '') { - val = background_image.top + parseInt($('#text_y').val()) - } - canvas.getActiveObject().set('top', val) - canvas.renderAll() - recordState() - }) - $('#text_x').bind('input propertychange', function () { - if ($('#text_x').val() == '') { - $('#text_x').val(0) - } - let val = background_image.left - if ($('#text_x').val() != '') { - val = background_image.left + parseInt($('#text_x').val()) - } - canvas.getActiveObject().set('left', val) - canvas.renderAll() - recordState() - }) - $('#text_w').bind('input propertychange', function () { - let val = '0' - if ($('#text_w').val() != '') { - val = $('#text_w').val() - } - let type = objs[getIndex(canvas.getActiveObject()) - 1].type - if (type == 3 || type == 4) { - canvas.getActiveObject().set('width', parseInt(val)) - } else { - canvas.getActiveObject().set('scaleX', parseInt(val) / canvas.getActiveObject().get('width')) - } - - canvas.renderAll() - recordState() - }) - $('#text_h').bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()) - if (objs[idx - 1].type == 7) { - if ($('#text_h').val() == '') { - $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) - } - canvas.getActiveObject().set('strokeWidth', parseInt($('#text_h').val())) - canvas.renderAll() - return - } - let val = '0' - if ($('#text_h').val() != '') { - val = $('#text_h').val() - } - let type = objs[getIndex(canvas.getActiveObject()) - 1].type - if (type == 3 || type == 4) { - canvas.getActiveObject().set('height', parseInt(val)) - } else { - canvas.getActiveObject().set('scaleY', parseInt(val) / canvas.getActiveObject().get('height')) - } - - canvas.renderAll() - recordState() - }) - $('#text_r').bind('input propertychange', function () { - if ($('#text_r').val() == '') { - $('#text_r').val('0') - } - $('#text_r').val(parseInt($('#text_r').val()) % 360) - //canvas.getActiveObject().set("rotate", val) - canvas.getActiveObject().rotate(parseInt($('#text_r').val())) - canvas.renderAll() - recordState() - }) - $('#out_pic_field_name').bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].name = $('#out_pic_field_name').val() - updateList() - recordState() - }) - - $('#text_black').click(function () { - let idx = getIndex(canvas.getActiveObject()) - if ($(this).prop('checked')) { - objs[idx - 1].black = true - } else { - objs[idx - 1].black = false - } - recordState() - }) - // $("#out_pic_black").click(function () { - // let idx = getIndex(canvas.getActiveObject()); - // if ($(this).prop("checked")) { - // objs[idx-1].black = true; - // } else { - // objs[idx-1].black = false; - // } - // recordState(); - // }); - $('#out_text_field_name').bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].name = $('#out_text_field_name').val() - updateList() - recordState() - }) - $('#qrcode_field_name').bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].name = $('#qrcode_field_name').val() - updateList() - recordState() - }) - $('#qrcode_color').bind('input propertychange', function () { - let item = canvas.getActiveObject() - let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0, foreground: $(this).val() }) - let idx = getIndex(item) - objs[idx - 1].color = $(this).val() - item.setSrc(qr, function (img) { - canvas.renderAll() - recordState() - }) - }) - $("#qrcode_val").bind('change', function () { - let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' - let item = canvas.getActiveObject(); - let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); - let idx = getIndex(item); - objs[idx - 1].val = val; - item.setSrc(qr, function (img) { - canvas.renderAll(); - recordState(); - }); - }); - $('#barcode_color').bind('input propertychange', function () { - let item = canvas.getActiveObject() - let idx = getIndex(item) - objs[idx - 1].color = $(this).val() - JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) - let barcode = document.getElementById('barcode') - let bar = barcode.toDataURL('image/png') - item.setSrc(bar, function (img) { - canvas.renderAll() - recordState() - }) - }) - $('#barcode_field_name').bind('input propertychange', function () { - let idx = getIndex(canvas.getActiveObject()) - objs[idx - 1].name = $('#barcode_field_name').val() - updateList() - recordState() - }) - $('#text_color').bind('input propertychange', function () { - canvas.getActiveObject().set('fill', $('#text_color').val()) - canvas.renderAll() - recordState() - }) - $('#text_back_color').bind('input propertychange', function () { - canvas.getActiveObject().set('textBackgroundColor', $('#text_back_color').val()) - canvas.renderAll() - recordState() - }) - $('#out_text_back_color').bind('input propertychange', function () { - canvas.getActiveObject().set('textBackgroundColor', $('#out_text_back_color').val()) - - canvas.renderAll() - recordState() - }) - $("#text_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#text_size").val())) { - val = parseFloat($("#text_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#text_size").val(val) - canvas.getActiveObject().set("fontSize", val*dpi/72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - recordState(); - }) - $("#circle_text_color").bind('input propertychange', function () { - canvas.getActiveObject().set("fill", $("#circle_text_color").val()); - canvas.renderAll(); - recordState(); - }) - $("#circle_text_back_color").bind('input propertychange', function () { - canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); - - canvas.renderAll(); - recordState(); - }) - $("#circle_text_text").bind('input propertychange', function () { - let val = $("#circle_text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); - canvas.renderAll(); - updateList(); - recordState(); - }) - $("#circle_text_center").click(function () { - let idx = getIndex(canvas.getActiveObject()); - if ($(this).prop("checked")) { - objs[idx - 1].circleCenter = 1; - } else { - objs[idx - 1].circleCenter = 0; - } - recordState(); - }); - $('#out_text_color').bind('input propertychange', function () { - canvas.getActiveObject().set('fill', $('#out_text_color').val()) - canvas.renderAll() - recordState() - }) - $("#out_text_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#out_text_size").val())) { - val = parseFloat($("#out_text_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#out_text_size").val(val) - canvas.getActiveObject().set("fontSize", val*dpi/72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - recordState(); - }) - $('#out_text_text').bind('input propertychange', function () { - let val = $("#out_text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); - canvas.renderAll() - updateList() - recordState() - }) - $('#rect_color').bind('input propertychange', function () { - canvas.getActiveObject().set('fill', $('#rect_color').val()) - - canvas.renderAll() - recordState() - }) - $('#rect_stroke').bind('input propertychange', function () { - canvas.getActiveObject().set('stroke', $('#rect_stroke').val()) - - canvas.renderAll() - recordState() - }) - $('#rect_stroke_width').bind('input propertychange', function () { - if ($('#rect_stroke_width').val() == '') { - $('#rect_stroke_width').val('0') - } else { - $('#rect_stroke_width').val(parseInt($('#rect_stroke_width').val())) - } - canvas.getActiveObject().set('strokeWidth', parseInt($('#rect_stroke_width').val())) - canvas.renderAll() - recordState() - }) - $('#rect_radius').bind('input propertychange', function () { - if ($('#rect_radius').val() == '') { - $('#rect_radius').val('0') - } else { - $('#rect_radius').val(parseInt($('#rect_radius').val())) - } - canvas.getActiveObject().set('rx', parseInt($('#rect_radius').val())) - canvas.getActiveObject().set('ry', parseInt($('#rect_radius').val())) - canvas.renderAll() - recordState() - }) - $('#circle_color').bind('input propertychange', function () { - canvas.getActiveObject().set('fill', $('#circle_color').val()) - - canvas.renderAll() - recordState() - }) - $('#circle_stroke').bind('input propertychange', function () { - canvas.getActiveObject().set('stroke', $('#circle_stroke').val()) - - canvas.renderAll() - recordState() - }) - $('#circle_stroke_width').bind('input propertychange', function () { - if ($('#circle_stroke_width').val() == '') { - $('#circle_stroke_width').val('0') - } else { - $(this).val(parseInt($(this).val())) - } - canvas.getActiveObject().set('strokeWidth', parseInt($('#circle_stroke_width').val())) - canvas.renderAll() - recordState() - }) - $('#counter_start').bind('input propertychange', function () { - canvas.getActiveObject().set('text', $('#counter_start').val()) - let item = canvas.getActiveObject() - let idx = getIndex(item) - objs[idx - 1].start = $(this).val() - canvas.renderAll() - recordState() - }) - $('#counter_add').bind('input propertychange', function () { - let item = canvas.getActiveObject() - let idx = getIndex(item) - objs[idx - 1].add = $(this).val() - canvas.renderAll() - recordState() - }) - $('#counter_color').bind('input propertychange', function () { - canvas.getActiveObject().set('fill', $('#counter_color').val()) - canvas.renderAll() - recordState() - }) - $("#count_size").bind('input propertychange', function () { - let val = 1 - if (parseFloat($("#count_size").val())) { - val = parseFloat($("#count_size").val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#count_size").val(val) - canvas.getActiveObject().set("fontSize", val*dpi/72); - $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); - $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); - canvas.renderAll(); - recordState(); - }) - $('#line_color').bind('input propertychange', function () { - canvas.getActiveObject().set('stroke', $('#line_color').val()) - canvas.renderAll() - recordState() - }) - $('#line_dist').bind('input propertychange', function () { - if ($('#line_dist').val() == '') { - $('#line_dist').val(0) - } - canvas.getActiveObject().set('strokeDashArray', [parseInt($('#line_dist').val()), parseInt($('#line_dist').val())]) - canvas.renderAll() - recordState() - }) - $('#text_text').bind('input propertychange', function () { - let val = $("#text_text").val().trim() - canvas.getActiveObject().set("text", ` ${val} `); - canvas.renderAll() - updateList() - recordState() - }) - $('#barcode_val').bind('input propertychange', function () { - let item = canvas.getActiveObject() - let idx = getIndex(item) - objs[idx - 1].val = $(this).val() - JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) - - let barcode = document.getElementById('barcode') - let bar = barcode.toDataURL('image/png') - item.setSrc(bar, function (img) { - canvas.renderAll() - recordState() - }) - }) - $("#barcode_show").click(function () { - let item = canvas.getActiveObject(); - let idx = getIndex(canvas.getActiveObject()); - let val = $("#barcode_size").val() - if ($(this).prop("checked")) { - objs[idx - 1].show = true; - val = 0 - } else { - objs[idx - 1].show = false; - } - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val*dpi/72, font: objs[idx - 1].font }); - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - }); - $("#barcode_size").bind('input propertychange', function () { - let item = canvas.getActiveObject(); - let idx = getIndex(item); - let val = 1 - if (parseFloat($(this).val())) { - val = parseFloat($(this).val()) - if (val < 1) { - val = 1 - } else if (val > dpi) { - val = dpi - } else if (val%0.5!==0) { - val = Math.round(val*2)/2 - } - - } - $("#barcode_size").val(val) - objs[idx - 1].fontSize = val*dpi/72; - JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); - - let barcode = document.getElementById('barcode'); - let bar = barcode.toDataURL("image/png"); - item.setSrc(bar, function (img) { - canvas.renderAll(); - recordState(); - }); - }) - $('#count_font_family').on('change', function () { - canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) - canvas.renderAll() - recordState() - }) - $('#text_font_family').on('change', function () { - canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) - canvas.renderAll() - recordState() - }) - - $('#circle_text_font_family').on('change', function () { - canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) - canvas.renderAll() - recordState(); - }); - - $('#language_select').on('change', function () { - //语言选择 - langua_ge($(this).find('option:selected').val()) - s_lan = $(this).find('option:selected').val() - localStorage.setItem('lang', s_lan) - }) - $('#barcode_font_family').on('change', function () { - let item = canvas.getActiveObject() - let idx = getIndex(item) - objs[idx - 1].font = $(this).find('option:selected').text() - JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) - - let barcode = document.getElementById('barcode') - let bar = barcode.toDataURL('image/png') - item.setSrc(bar, function (img) { - canvas.renderAll() - recordState() - }) - //recordState(); - }) - $('#out_text_font_family').on('change', function () { - canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) - canvas.renderAll() - recordState() - }) - $('.obj_list').on('click', 'div', function () { - let idx = objs.length - 1 - $(this).index() - selectObj(idx) - }) - $('#exceed').click(function () { - let idx = getIndex(canvas.getActiveObject()) - if ($(this).prop('checked')) { - objs[idx - 1].exceed = true - } else { - objs[idx - 1].exceed = false - } - recordState() - }) - $('#autoWrap').click(function () { - let idx = getIndex(canvas.getActiveObject()) - if ($(this).prop('checked')) { - objs[idx - 1].autoWrap = true - } else { - objs[idx - 1].autoWrap = false - } - recordState() - }) - $('#fixSize').click(function () { - let idx = getIndex(canvas.getActiveObject()) - if ($(this).prop('checked')) { - objs[idx - 1].fixSize = true - } else { - objs[idx - 1].fixSize = false - } - recordState() - }) - $('#delete').click(function () { - let actObjs = canvas.getActiveObjects() - let allObjs = canvas.getObjects() - let flag = 0 - for (let item of actObjs) { - if (item != allObjs[0]) { - let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 - objs.splice(i - 1, 1) - canvas.remove(item) - flag = 1 - } - } - updateList() - canvas.discardActiveObject() - if (flag == 1) { - recordState() - canvas.renderAll() - } - // let actObjs = canvas.getActiveObjects(); - // let allObjs = canvas.getObjects(); - // let flag = 0; - // for( let item of actObjs){ - // if ( item != allObjs[0]){ - // let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 - // objs.splice(i-1,1); - // canvas.remove(item); - // flag = 1; - // updateList(); - // } - // } - // selectObj(-1); - // if(flag == 1){ - // recordState(); - // } - }) - - //在关于我们获取版本号。 - // 注意:function 前面加了 async 关键字 - $("#about").click(async function () { - - // 1. 先异步获取版本号 - let version = "0.0.0"; // 默认值,防止获取失败 - try { - if (window.sysAPI && window.sysAPI.getAppVersion) { - const v = await window.sysAPI.getAppVersion(); - version = "v" + v; // 拼接一个 v,变成 v2.3.017 - } - } catch (e) { - console.error("获取版本号失败", e); - } - - // 2. 获取成功后再弹出窗口 - layer.open({ - type: 1 - , title: language_str("about") - , area: '450px;' - , id: 'LAY_layuipro' - , moveType: 1 - , content: '
' + - '
' + - '
Soon Design
' + - // 【修改点】这里使用了刚才获取到的 version 变量 - '
' + language_str("vers") + ' ' + version + '
' + - '
' + language_str("copyright") + '
' + - '
' - }); - }) - $("#help").click(() => { - ipcRenderer.send('open-help-file'); - }); - function handleObjectSelected() { - let select_obj = canvas.getActiveObject() - if (!select_obj) { - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) //comfirm - return - } - let i = getIndex(select_obj) - if (i != 0) { - //不是背景 - //console.log(objs); - --i - switch (objs[i].type) { - case 1: - //console.log("图片"); - $('#component_type').text(language_str('img')) //"图片" - selectObject(select_obj, i) - selectPic(select_obj, i) - break - case 2: - //console.log("预留图片"); - - $('#component_type').text(language_str('simg')) //"合成图片" - selectObject(select_obj, i) - selectOutPic(select_obj, i) - break - case 3: - $('#component_type').text(language_str('text')) //"文字" - //console.log("文字"); - selectObject(select_obj, i) - selectText(select_obj, i) - break - case 4: - //console.log("标签"); - - $('#component_type').text(language_str('stext')) //"合成文字" - selectObject(select_obj, i) - selectOutText(select_obj, i) - break - case 5: - //console.log("矩形"); - - $('#component_type').text(language_str('rect')) //"矩形" - selectObject(select_obj, i) - selectRect(select_obj, i) - break - case 6: - //console.log("圆形"); - - $('#component_type').text(language_str('circ')) //"圆形" - selectObject(select_obj, i) - selectCircle(select_obj, i) - break - case 7: - //console.log("直线"); - - $('#component_type').text(language_str('line')) //"直线" - selectObject(select_obj, i) - selectLine(select_obj, i) - break - case 8: - //console.log("二维码"); - - $('#component_type').text(language_str('qrc')) //"二维码" - selectObject(select_obj, i) - selectQrCode(select_obj, i) - break - case 9: - //console.log("条形码"); - - $('#component_type').text(language_str('barc')) //"条形码" - selectObject(select_obj, i) - selectBarCode(select_obj, i) - break - case 10: - //console.log("计数器"); - - $('#component_type').text(language_str('counter')) //"计数器" - selectObject(select_obj, i) - selectCounter(select_obj, i) - break - case 11: - - $("#component_type").text(language_str("ctext"));//"圆形文字" - //console.log("圆形文字"); - selectObject(select_obj, i); - selectCircleText(select_obj, i); - break; - } - } else { - //背景,收起对象属性 - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#component_type').text(language_str('bg')) - } - } - - canvas1.on('mouse:move', (e) => { - let width = canvas.getWidth() - let height = canvas.getHeight() - $('#posText').text( - 'X: ' + (e.pointer.x - (width / 2 - (width / 2 - background_image.left) * zoom)).toFixed(0) + ' Y: ' + (e.pointer.y - (height / 2 - (height / 2 - background_image.top) * zoom)).toFixed(0) - ) - }) - canvas1.on("object:moving", function (e) { - let obj = canvas1.getActiveObject() - let idx = getIndex(e.target, canvas1); - // 添加边界检查,避免访问 undefined - if (idx > 0 && idx <= objs1.length && objs1[idx - 1] && objs1[idx - 1].type && objs1[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { - let pointer = { - x: obj.left, - y: obj.top - } - let distance = getDisdance(pointer.x, pointer.y, canvas1.width/2, canvas1.height/2) - let angleDeg = getDeg(pointer) - let flipped = pointer.y > canvas1.height/2 ? true : false - circle_text_diameter.setValue(distance*2-50); - obj.set("diameter", distance*2); - obj.set("angle", angleDeg); - obj.set("flipped", flipped); - } - updateControls(); - }); - // 监听键盘事件 - - // 复制选中对象 - function copySelection() { - const activeObjects = canvas.getActiveObjects(); - - if (!activeObjects || activeObjects.length === 0) { - console.log("未选中任何对象"); - layer.msg("未选中任何对象") - return; - } - - // 存储原始对象用于克隆 - clipboardData.objects = activeObjects; - console.log(activeObjects) - console.log(`已复制 ${activeObjects.length} 个对象`); - } - - // 粘贴对象 - function pasteFromClipboard() { - if (!clipboardData.objects || clipboardData.objects.length === 0) { - console.log("剪贴板为空"); - layer.msg("剪贴板为空") - return; - } - - // 克隆所有对象 - fabric.util.enlivenObjects( - clipboardData.objects.map(obj => obj.toObject()), - clonedObjects => { - // 设置新位置(添加偏移) - clonedObjects.forEach((clone, index) => { - let idx = getIndex(clipboardData.objects[index]) - console.log(clone) - clone.set({ - left: clone.left + clipboardData.offset, - top: clone.top + clipboardData.offset, - evented: true - }); - // 计算整体偏移 - const groupOffset = clone.group; - if (groupOffset) { - clone.set({ - left: groupOffset.left + clipboard.offset, - top: groupOffset.top + clipboard.offset - }); - } - - // 添加到画布 - canvas.add(clone); - let tobj = {...objs[idx-1]} - if (tobj.name) tobj.name = tobj.name + objs.length - objs.push(tobj); - canvas.renderAll(); - updateList(); - recordState(); - }); - - // 选中粘贴的对象 - canvas.discardActiveObject(); - const sel = new fabric.ActiveSelection(clonedObjects, { - canvas: canvas - }); - canvas.setActiveObject(sel); - canvas.requestRenderAll(); - selectObj(objs.length - 1); - // 增加下次偏移 - clipboardData.offset += 10; - } - ); - } - canvas1.on('object:scaling', function (event) { - //if(objs[getIndex(event.target) - 1].type == 5 || objs[getIndex(event.target) - 1].type == 6){ - // //canvas.getActiveObject().set("width", canvas.getActiveObject().get("width") * canvas.getActiveObject().get("scaleX")); - // //canvas.getActiveObject().set("height", canvas.getActiveObject().get("height") * canvas.getActiveObject().get("scaleY")); - // //canvas.getActiveObject().set("scaleX", 1); - // //canvas.getActiveObject().set("scaleY", 1); - // //console.log(canvas.getActiveObject().get("scaleX")); - // //canvas1.discardActiveObject(); - // //canvas1.setActiveObject(event.target); - // //canvas1.renderAll(); - // //updateControls(); - //} - }) - canvas1.on('object:rotating', function (e) { - updateControls() - }) - canvas1.on('object:rotated', function (e) { - console.log('object:rotated') - recordState() - }) - canvas1.on('object:added', function (e) { - //console.log("object:added"); - //recordState(); - ////console.log(objs); - }) - canvas1.on('mouse:down', (e) => { - //单选 - let select_obj = canvas.getActiveObject() - console.log(select_obj) - let i = getIndex(select_obj) - if (i != 0) { - //不是背景 - //console.log(objs); - --i - switch (objs[i].type) { - case 1: - //console.log("图片"); - $('#component_type').text(language_str('img')) //"图片" - selectObject(select_obj, i) - selectPic(select_obj, i) - break - case 2: - //console.log("预留图片"); - - $('#component_type').text(language_str('simg')) //"合成图片" - selectObject(select_obj, i) - selectOutPic(select_obj, i) - break - case 3: - $('#component_type').text(language_str('text')) //"文字" - //console.log("文字"); - selectObject(select_obj, i) - selectText(select_obj, i) - break - case 4: - //console.log("标签"); - - $('#component_type').text(language_str('stext')) //"合成文字" - selectObject(select_obj, i) - selectOutText(select_obj, i) - break - case 5: - //console.log("矩形"); - - $('#component_type').text(language_str('rect')) //"矩形" - selectObject(select_obj, i) - selectRect(select_obj, i) - break - case 6: - //console.log("圆形"); - - $('#component_type').text(language_str('circ')) //"圆形" - selectObject(select_obj, i) - selectCircle(select_obj, i) - break - case 7: - //console.log("直线"); - - $('#component_type').text(language_str('line')) //"直线" - selectObject(select_obj, i) - selectLine(select_obj, i) - break - case 8: - //console.log("二维码"); - - $('#component_type').text(language_str('qrc')) //"二维码" - selectObject(select_obj, i) - selectQrCode(select_obj, i) - break - case 9: - //console.log("条形码"); - - $('#component_type').text(language_str('barc')) //"条形码" - selectObject(select_obj, i) - selectBarCode(select_obj, i) - break - case 10: - //console.log("计数器"); - - $('#component_type').text(language_str('counter')) //"计数器" - selectObject(select_obj, i) - selectCounter(select_obj, i) - break - case 11: - - $("#component_type").text(language_str("ctext"));//"圆形文字" - //console.log("圆形文字"); - selectObject(select_obj, i); - selectCircleText(select_obj, i); - break; - } - selectObj(i, true) //如果是点击画布中,则相应列表 - } else { - //背景,收起对象属性 - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#counter_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - selectObj(-1) - $('#component_type').text(language_str('bg')) - } - }) - canvas1.on('text:editing:entered', function () { - _textEdit = true - }) - canvas1.on('text:editing:exited', function () { - _textEdit = false - }) - canvas1.on('object:modified', function (event) { - if (canvas1.getActiveObjects().length > 1) { - return - } - if (objs[getIndex(event.target) - 1].type == 3) { - event.target.fontSize *= event.target.scaleX - event.target.fontSize = event.target.fontSize.toFixed(0) - event.target.scaleX = 1 - event.target.scaleY = 1 - event.target._clearCache() - // $('#text_size').val(event.target.fontSize) - canvas1.renderAll() - updateControls() - } else if (objs[getIndex(event.target) - 1].type == 4) { - event.target.fontSize *= event.target.scaleX - event.target.fontSize = event.target.fontSize.toFixed(0) - event.target.scaleX = 1 - event.target.scaleY = 1 - event.target._clearCache() - // $('#out_text_size').val(event.target.fontSize) - canvas1.renderAll() - } else if (objs[getIndex(event.target) - 1].type == 10) { - event.target.fontSize *= event.target.scaleX - event.target.fontSize = event.target.fontSize.toFixed(0) - event.target.scaleX = 1 - event.target.scaleY = 1 - event.target._clearCache() - // $('#count_size').val(event.target.fontSize) - canvas1.renderAll() - } else if (objs[getIndex(event.target) - 1].type == 5) { - canvas1.getActiveObject().set('width', canvas1.getActiveObject().get('width') * canvas1.getActiveObject().get('scaleX')) - canvas1.getActiveObject().set('height', canvas1.getActiveObject().get('height') * canvas1.getActiveObject().get('scaleY')) - canvas1.getActiveObject().set('scaleX', 1) - canvas1.getActiveObject().set('scaleY', 1) - updateControls() - } - recordState() - }) - canvas1.on('mouse:up', (e) => { - //HERE - switch (addState) { - case 1: - addPic(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addPic').css('background-color', '') - break - case 2: - addOutPic(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addOutPic').css('background-color', '') - break - case 3: - addText(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addText').css('background-color', '') - break - case 4: - addOutText(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addOutText').css('background-color', '') - break - case 5: - addRect(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addRect').css('background-color', '') - break - case 6: - addCircle(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addCircle').css('background-color', '') - break - case 7: - addLine(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addLine').css('background-color', '') - break - case 8: - addQrCode(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addQrCode').css('background-color', '') - break - case 9: - addBarCode(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas1.renderAll() - $('#addBarCode').css('background-color', '') - break - case 10: - addCounter(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - $('#addCounter').css('background-color', '') - break - case 11: - addCircleText(e.pointer); - addState = 0; - background_image.hoverCursor = "default"; - canvas1.renderAll(); - $("#addCircleText").css("background-color", ""); - break; - } - }) - canvas1.on('text:changed', (e) => { - $('#text_text').val(e.target.get('text')) - }) - canvas1.on('after:render', function () { - if (is_bgi_add) { - var image = document.getElementById('source_front') - ctx1.drawImage(image, (canvas.width - $('#source_front').width()) / 2, (canvas.height - $('#source_front').height()) / 2, $('#source_front').width(), $('#source_front').height()) - - /* - 实际905 * 534 - 1052 * - */ - } - }) - canvas2.on('text:editing:entered', function () { - _textEdit = true - }) - canvas2.on('text:editing:exited', function () { - _textEdit = false - }) - canvas2.on('after:render', function () { - if (is_bgi_add) { - var image = document.getElementById('source_back') - ctx2.drawImage(image, (canvas.width - $('#source_back').width()) / 2, (canvas1.height - $('#source_back').height()) / 2, $('#source_back').width(), $('#source_back').height()) - } - }) - canvas2.on('mouse:move', (e) => { - $('#posText').text('X: ' + (e.pointer.x - background_image.left).toFixed(0) + ' Y: ' + (e.pointer.y - background_image.top).toFixed(0)) - }) - canvas2.on("object:moving", function (e) { - let obj = canvas2.getActiveObject() - let idx = getIndex(e.target, canvas2); - // 添加边界检查,避免访问 undefined - if (idx > 0 && idx <= objs2.length && objs2[idx - 1] && objs2[idx - 1].type && objs2[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { - let pointer = { - x: obj.left, - y: obj.top - } - let distance = getDisdance(pointer.x, pointer.y, canvas2.width/2, canvas2.height/2) - // 计算角度时需要使用 canvas2 - let centerX = canvas2.width / 2; - let centerY = canvas2.height / 2; - let mouseX = pointer.x; - let mouseY = pointer.y; - let dx = mouseX - centerX; - let dy = mouseY - centerY; - let angleRad = Math.atan2(dy, dx); - let angleDeg = angleRad * (180/Math.PI); - if (angleDeg < 0) { - angleDeg += 360; - } - angleDeg += 90; - let flipped = pointer.y > canvas2.height/2 ? true : false - circle_text_diameter.setValue(distance*2-50); - obj.set("diameter", distance*2); - obj.set("angle", angleDeg); - obj.set("flipped", flipped); - } - updateControls(); - }); - canvas2.on('object:scaling', function (e) { - updateControls() - }) - canvas2.on('object:rotating', function (e) { - updateControls() - }) - canvas2.on('object:rotated', function (e) { - console.log('object:rotated') - recordState() - }) - canvas2.on('object:added', function (e) { - //console.log("object:added"); - //recordState(); - ////console.log(objs); - }) - canvas2.on('mouse:down', (e) => { - //单选 - let select_obj = canvas.getActiveObject() - let i = getIndex(select_obj) - if (i != 0) { - //不是背景 - //console.log(objs); - --i - switch (objs[i].type) { - case 1: - //console.log("图片"); - $('#component_type').text(language_str('img')) //"图片" - selectObject(select_obj, i) - selectPic(select_obj, i) - break - case 2: - //console.log("预留图片"); - - $('#component_type').text(language_str('simg')) //"合成图片" - selectObject(select_obj, i) - selectOutPic(select_obj, i) - break - case 3: - $('#component_type').text(language_str('text')) //"文字" - //console.log("文字"); - selectObject(select_obj, i) - selectText(select_obj, i) - break - case 4: - //console.log("标签"); - - $('#component_type').text(language_str('stext')) //"合成文字" - selectObject(select_obj, i) - selectOutText(select_obj, i) - break - case 5: - //console.log("矩形"); - - $('#component_type').text(language_str('rect')) //"矩形" - selectObject(select_obj, i) - selectRect(select_obj, i) - break - case 6: - //console.log("圆形"); - - $('#component_type').text(language_str('circ')) //"圆形" - selectObject(select_obj, i) - selectCircle(select_obj, i) - break - case 7: - //console.log("直线"); - - $('#component_type').text(language_str('line')) //"直线" - selectObject(select_obj, i) - selectLine(select_obj, i) - break - case 8: - //console.log("二维码"); - - $('#component_type').text(language_str('qrc')) //"二维码" - selectObject(select_obj, i) - selectQrCode(select_obj, i) - break - case 9: - //console.log("条形码"); - - $('#component_type').text(language_str('barc')) //"条形码" - selectObject(select_obj, i) - selectBarCode(select_obj, i) - break - case 10: - //console.log("计数器"); - - $('#component_type').text(language_str('counter')) //"计数器" - selectObject(select_obj, i) - selectCounter(select_obj, i) - break - case 11: - - $("#component_type").text(language_str("ctext"));//"圆形文字" - //console.log("圆形文字"); - selectObject(select_obj, i); - selectCircleText(select_obj, i); - break; - } - selectObj(i, true) //如果是点击画布中,则相应列表 - } else { - //背景,收起对象属性 - $('#base_control').hide() - $('#line_control').hide() - $('#pic_control').hide() - $('#out_pic_control').hide() - $('#text_control').hide() - $("#circle_text_control").hide(); - $('#out_text_control').hide() - $('#rect_control').hide() - $('#circle_control').hide() - $('#qrcode_Controll').hide() - $('#barcode_controll').hide() - $('#counter_control').hide() - selectObj(-1) - $('#component_type').text(language_str('bg')) - } - }) - canvas2.on('object:modified', function (event) { - if (canvas.getActiveObjects().length > 1) { - return - } - if (objs[getIndex(event.target) - 1].type == 3) { - event.target.fontSize *= event.target.scaleX - event.target.fontSize = event.target.fontSize.toFixed(0) - event.target.scaleX = 1 - event.target.scaleY = 1 - event.target._clearCache() - // $('#text_size').val(event.target.fontSize) - canvas2.renderAll() - } else if (objs[getIndex(event.target) - 1].type == 4) { - event.target.fontSize *= event.target.scaleX - event.target.fontSize = event.target.fontSize.toFixed(0) - event.target.scaleX = 1 - event.target.scaleY = 1 - event.target._clearCache() - // $('#out_text_size').val(event.target.fontSize) - canvas2.renderAll() - } else if (objs[getIndex(event.target) - 1].type == 10) { - event.target.fontSize *= event.target.scaleX - event.target.fontSize = event.target.fontSize.toFixed(0) - event.target.scaleX = 1 - event.target.scaleY = 1 - event.target._clearCache() - // $('#count_size').val(event.target.fontSize) - canvas2.renderAll() - } else if (objs[getIndex(event.target) - 1].type == 5) { - canvas2.getActiveObject().set('width', canvas2.getActiveObject().get('width') * canvas2.getActiveObject().get('scaleX')) - canvas2.getActiveObject().set('height', canvas2.getActiveObject().get('height') * canvas2.getActiveObject().get('scaleY')) - canvas2.getActiveObject().set('scaleX', 1) - canvas2.getActiveObject().set('scaleY', 1) - updateControls() - } - console.log('object:modified') - recordState() - }) - canvas2.on('mouse:up', (e) => { - //HERE - switch (addState) { - case 1: - addPic(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addPic').css('background-color', '') - break - case 2: - addOutPic(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addOutPic').css('background-color', '') - break - case 3: - addText(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addText').css('background-color', '') - break - case 4: - addOutText(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addOutText').css('background-color', '') - break - case 5: - addRect(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addRect').css('background-color', '') - break - case 6: - addCircle(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addCircle').css('background-color', '') - break - case 7: - addLine(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addLine').css('background-color', '') - break - case 8: - addQrCode(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addQrCode').css('background-color', '') - break - case 9: - addBarCode(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addBarCode').css('background-color', '') - break - case 10: - addCounter(e.pointer) - addState = 0 - background_image.hoverCursor = 'default' - canvas2.renderAll() - $('#addCounter').css('background-color', '') - break - case 11: - addCircleText(e.pointer); - addState = 0; - background_image.hoverCursor = "default"; - canvas2.renderAll(); - $("#addCircleText").css("background-color", ""); - break; - } - }) - var circle_text_size = slider.render({ - elem: '#circle_text_size', - min: 10, - max: 120 - , change: function (value) { - canvas.getActiveObject().set("fontSize", value); - canvas.renderAll(); - } - }); - var circle_text_diameter = slider.render({ - elem: '#circle_text_diameter', - min: 50, - max: 1100 - , change: function (value) { - canvas.getActiveObject().set("diameter", value); - canvas.renderAll(); - } - }); - var out_pic_op = slider.render({ - elem: '#out_pic_op', - change: function (value) { - console.log(value) - console.log(canvas.getActiveObject()) - canvas.getActiveObject().set('opacity', (100 - value) / 100) - canvas.renderAll() - } - }) - var pic_op = slider.render({ - elem: '#pic_op', - change: function (value) { - console.log(value) - console.log(canvas.getActiveObject()) - canvas.getActiveObject().set('opacity', (100 - value) / 100) - canvas.renderAll() - } - }) - var rect_op = slider.render({ - elem: '#rect_op', - change: function (value) { - console.log(value) - console.log(canvas.getActiveObject()) - canvas.getActiveObject().set('opacity', (100 - value) / 100) - canvas.renderAll() - } - }) - var circle_op = slider.render({ - elem: '#circle_op', - change: function (value) { - console.log(value) - console.log(canvas.getActiveObject()) - canvas.getActiveObject().set('opacity', (100 - value) / 100) - canvas.renderAll() - } - }) - - // 标尺绘制函数 - function initRulers() { - const dpi = 300; - const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) - const rulerHeight = 20; - const rulerWidth = 20; - - // 获取实际画布的像素尺寸(px) - // design2.html 的实际画布尺寸是 1012px x 648px - const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) - const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) - - // 获取显示区域的尺寸(包括 margin) - const canvasDiv = $('#canvas-div'); - const canvasWrapper = $('.canvas-wrapper'); - // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 - let canvasWidth = canvasWrapper.width(); - let canvasHeight = canvasWrapper.height(); - if (!canvasWidth || canvasWidth === 0) { - const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; - canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px - } - if (!canvasHeight || canvasHeight === 0) { - const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; - canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px - } - - // 设置标尺尺寸(使用显示区域的尺寸) - $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); - $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); - $('.ruler-horizontal').css('width', canvasWidth + 'px'); - $('.ruler-vertical').css('height', canvasHeight + 'px'); - - // 绘制水平标尺 - function drawHorizontalRuler() { - const ctx = document.getElementById('ruler-h').getContext('2d'); - ctx.clearRect(0, 0, canvasWidth, rulerHeight); - - // 背景 - ctx.fillStyle = '#2C2C2C'; - ctx.fillRect(0, 0, canvasWidth, rulerHeight); - - // 边框 - ctx.strokeStyle = '#1E1E1E'; - ctx.lineWidth = 1; - ctx.strokeRect(0, 0, canvasWidth, rulerHeight); - ctx.beginPath(); - ctx.moveTo(0, rulerHeight - 1); - ctx.lineTo(canvasWidth, rulerHeight - 1); - ctx.stroke(); - - // 刻度线 - ctx.strokeStyle = '#888888'; - ctx.fillStyle = '#CCCCCC'; - ctx.font = '10px Arial'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 - // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) - // 显示区域宽度 = actualCanvasWidth * zoom (px) - // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 - const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 - const startX = 0; - const endX = canvasWidth; - - // 绘制主要刻度(每10mm) - for (let mm = 0; mm * pxPerMm <= endX; mm += 10) { - const x = startX + mm * pxPerMm; - if (x > endX) break; - - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, rulerHeight); - ctx.stroke(); - - // 数字 - ctx.fillText(mm + 'mm', x + 2, 2); - } - - // 绘制次要刻度(每1mm) - for (let mm = 0; mm * pxPerMm <= endX; mm += 1) { - const x = startX + mm * pxPerMm; - if (x > endX) break; - if (mm % 10 === 0) continue; // 跳过主要刻度 - - ctx.beginPath(); - ctx.moveTo(x, rulerHeight - 8); - ctx.lineTo(x, rulerHeight); - ctx.stroke(); - } - } - - // 绘制垂直标尺 - function drawVerticalRuler() { - const ctx = document.getElementById('ruler-v').getContext('2d'); - ctx.clearRect(0, 0, rulerWidth, canvasHeight); - - // 背景 - ctx.fillStyle = '#2C2C2C'; - ctx.fillRect(0, 0, rulerWidth, canvasHeight); - - // 边框 - ctx.strokeStyle = '#1E1E1E'; - ctx.lineWidth = 1; - ctx.strokeRect(0, 0, rulerWidth, canvasHeight); - ctx.beginPath(); - ctx.moveTo(rulerWidth - 1, 0); - ctx.lineTo(rulerWidth - 1, canvasHeight); - ctx.stroke(); - - // 刻度线 - ctx.strokeStyle = '#888888'; - ctx.fillStyle = '#CCCCCC'; - ctx.font = '10px Arial'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 - const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 - const startY = 0; - const endY = canvasHeight; - - // 绘制主要刻度(每10mm) - for (let mm = 0; mm * pxPerMm <= endY; mm += 10) { - const y = startY + mm * pxPerMm; - if (y > endY) break; - - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(rulerWidth, y); - ctx.stroke(); - - // 数字(旋转90度) - ctx.save(); - ctx.translate(2, y + 8); - ctx.rotate(-Math.PI / 2); - ctx.fillText(mm + 'mm', 0, 0); - ctx.restore(); - } - - // 绘制次要刻度(每1mm) - for (let mm = 0; mm * pxPerMm <= endY; mm += 1) { - const y = startY + mm * pxPerMm; - if (y > endY) break; - if (mm % 10 === 0) continue; // 跳过主要刻度 - - ctx.beginPath(); - ctx.moveTo(rulerWidth - 8, y); - ctx.lineTo(rulerWidth, y); - ctx.stroke(); - } - } - - // 初始绘制 - drawHorizontalRuler(); - drawVerticalRuler(); - - // 监听窗口大小变化 - $(window).on('resize', function() { - setTimeout(function() { - const canvasWrapper = $('.canvas-wrapper'); - let newWidth = canvasWrapper.width(); - let newHeight = canvasWrapper.height(); - if (!newWidth || newWidth === 0) { - const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; - newWidth = canvas1Width + 20; // 加上 margin-left: 20px - } - if (!newHeight || newHeight === 0) { - const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; - newHeight = canvas1Height + 20; // 加上 margin-top: 20px - } - if (newWidth !== canvasWidth || newHeight !== canvasHeight) { - canvasWidth = newWidth; - canvasHeight = newHeight; - $('#ruler-h').attr('width', newWidth); - $('#ruler-v').attr('height', newHeight); - $('.ruler-horizontal').css('width', newWidth + 'px'); - $('.ruler-vertical').css('height', newHeight + 'px'); - drawHorizontalRuler(); - drawVerticalRuler(); - } - }, 100); - }); - - // 监听画布缩放变化 - canvas1.on('after:render', function() { - drawHorizontalRuler(); - drawVerticalRuler(); - }); - canvas2.on('after:render', function() { - drawHorizontalRuler(); - drawVerticalRuler(); - }); - } - }) + + function desEncrypt(str, key = 'df6a551ca43181fc485f3043bcdd2fbc') { + var APIFMS + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key) + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }) + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext) + } catch (err) { + console.log('des 加密 -------------------------') + console.log(err) + } + return APIFMS + } + + (function (fabric) { + fabric.CurvedText = fabric.util.createClass(fabric.Object, { + type: 'curved-text', + diameter: 250, + kerning: 0, + text: '', + flipped: false, + fill: '#000', + fontFamily: 'Times New Roman', + fontSize: 24, // in px + fontWeight: 'normal', + fontStyle: '', // "normal", "italic" or "oblique". + cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), + strokeStyle: null, + strokeWidth: 0, + initialize: function (text, options) { + options || (options = {}); + if (typeof text === 'object' && text) { + for (let key in text) { + this[key] = text[key] + } + } else { + this.text = text; + } + this.callSuper('initialize', options); + this.set('lockUniScaling', true); + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + }, + _getFontDeclaration: function () { + return [ + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + cropCanvas: function (canvas) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + pix = { x: [], y: [] }, n, + imageData = ctx.getImageData(0, 0, w, h), + fn = function (a, b) { return a - b }; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + if (imageData.data[((y * w + x) * 4) + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // 如果没有找到任何非透明像素,直接返回,不进行裁剪 + if (pix.x.length === 0 || pix.y.length === 0) { + return; + } + pix.x.sort(fn); + pix.y.sort(fn); + n = pix.x.length - 1; + w = pix.x[n] - pix.x[0]; + h = pix.y[n] - pix.y[0]; + // 确保宽度和高度是有效的正数 + if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { + return; + } + var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); + canvas.width = w; + canvas.height = h; + ctx.putImageData(cut, 0, 0); + }, + getCircularText: function () { + var text = this.text, + diameter = this.diameter, + flipped = this.flipped, + kerning = this.kerning, + fill = this.fill, + inwardFacing = true, + startAngle = 0, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'), + cw, // character-width + x, // iterator + clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise + if (flipped) { + // startAngle = 180; + inwardFacing = false; + } + startAngle *= Math.PI / 180; // convert to radians + var d = document.createElement('div'); + d.style.fontFamily = this.fontFamily; + d.style.fontSize = this.fontSize + 'px'; + d.style.fontWeight = this.fontWeight; + d.style.fontStyle = this.fontStyle; + d.textContent = text; + document.body.appendChild(d); + var textHeight = d.offsetHeight; + document.body.removeChild(d); + canvas.width = canvas.height = diameter; + ctx.font = this._getFontDeclaration(); + if (inwardFacing) { text = text.split('').reverse().join('') }; + ctx.translate(diameter / 2, diameter / 2); // Move to center + startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward + ctx.textBaseline = 'middle'; // Ensure we draw in exact center + ctx.textAlign = 'center'; // Ensure we draw in exact center + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; + startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; + } + ctx.rotate(startAngle); + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; // half letter + ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); + if (this.strokeStyle && this.strokeWidth) { + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.miterLimit = 2; + ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + } + ctx.fillStyle = fill; + ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter + } + return canvas; + }, + _set: function (key, value) { + switch (key) { + case 'scaleX': + this.fontSize *= value; + this.diameter *= value; + this.width *= value; + this.scaleX = 1; + if (this.width < 1) { this.width = 1; } + break; + case 'scaleY': + this.height *= value; + this.scaleY = 1; + if (this.height < 1) { this.height = 1; } + break; + default: + this.callSuper('_set', key, value); + break; + } + }, + _render: function (ctx) { + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); + this.setCoords(); + }, + toObject: function (propertiesToInclude) { + return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); + } + }); + fabric.CurvedText.fromObject = function (object, callback, forceAsync) { + return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); + }; + })(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); + + const remote = require('@electron/remote') + var path = require('path'); + // 使用用户数据目录,避免写入需要管理员权限 + const exePath = remote.app.getPath('userData') + const { ipcRenderer } = require('electron') + const { dialog } = require('@electron/remote') + var jrQrcode = require('jr-qrcode') + var JsBarcode = require('jsbarcode') + ipcRenderer.send('get-sys-fonts') + ipcRenderer.send('get-scale-rate') + const { clipboard } = require('electron') + const dpi = 300 + // 内部剪贴板 + let clipboardData = { + objects: null, + offset: 10 // 粘贴位置偏移量 + }; + //var encrypt = new JSEncrypt(); + //encrypt.setPublicKey(`-----BEGIN PUBLIC KEY----- + //MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDAbKQ46WevL9PapJ+AlZqSzczkTovu7STH + //FmZmIJcNrwPPEWRR2PliUvtS28V+gNMWlu3wNWFAS5KGDbTtndIrP3hnlp9BDIcAcdNuJUM7 + //yJfAYBhtkqvj45Orvdd2RYRxhkQb34uTNXpM+biMH6Wd0wykNb9/VBNM0C12KswEdwIBEQ== + //-----END PUBLIC KEY-----`); + layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { + let myDate = new Date() + let s_lan = '' + ipcRenderer.send('get-sys-language') + /* if(myDate.getFullYear()>2021 && myDate.getMonth() >= 1 && myDate.getDate() > 15){ + + }*/ + function GetFile() { + const params = new URLSearchParams(window.location.search); + return params + } + function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)) + }) + + $("[language='t']").each(function (i) { + $(this).attr('title', $(this).attr(lan)) + }) + } + function language_str(str) { + let t = { + whetherSave: { zh: '是否保存当前文件?', ozh: '是否保存當前文件?', en: 'Do you want to save the current file?' }, + save: { zh: '保存', ozh: '保存', en: 'Save' }, + noSave: { zh: '不保存', ozh: '不保存', en: 'No saving' }, + cancel: { zh: '取消', ozh: '取消', en: 'Cancel' }, + saveSucc: { zh: '保存成功至', ozh: '保存成功至', en: 'Save successfully to' }, + saveFile: { zh: '保存文件', ozh: '保存文件', en: 'Save file' }, + display: { zh: '预览', ozh: '預覽', en: 'Display' }, + output: { zh: '导出', ozh: '導出', en: 'Export' }, + bg: { zh: '背景', ozh: '背景', en: 'Background' }, + selectPic: { zh: '请选择图片', ozh: '請選擇圖片', en: 'Please select a picture' }, + selectPic: { zh: '请选择文件', ozh: '請選擇文件', en: 'Please select a file' }, + img: { zh: '圖片', ozh: '圖片', en: 'Image' }, + simg: { zh: '合成圖片', ozh: '合成圖片', en: 'Synthesized Image' }, + text: { zh: '文本', ozh: '文本', en: 'Text' }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + stext: { zh: '合成文本', ozh: '合成文本', en: 'Synthesized Text' }, + rect: { zh: '矩形', ozh: '矩形', en: 'Rectangle' }, + circ: { zh: '圓形', ozh: '圓形', en: 'Circle' }, + line: { zh: '直線', ozh: '直線', en: 'Line' }, + qrc: { zh: '二維碼', ozh: '二維碼', en: 'QR code' }, + barc: { zh: '條形碼', ozh: '條形碼', en: 'Barcode' }, + counter: { zh: '計數器', ozh: '计数器', en: 'Counter' }, + about: { zh: '关于Soon Design', ozh: '關於Soon Design', en: 'About Soon Design' }, + vers: { zh: '版本', ozh: '版本', en: 'Version' }, + comf: { zh: '确认', ozh: '確認', en: 'Select' }, + copyright: { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + } + return t[str][s_lan] + } + + var $ = layui.$ + var layer = layui.layer, + slider = layui.slider, + colorpicker = layui.colorpicker + let addState = 0 //0是不添加 + let background_image, background_image1, background_image2 + let bg_version = 1 //front_bg1.png front_bg2.png + var zoom = 0 + $('#canvas1').attr('height', document.body.clientHeight - 100) + $('#canvas2').attr('height', document.body.clientHeight - 100) + $('#object_attribute').css('height', '470px') + $('#obj_list').css('height', document.body.clientHeight - 520) + $('.obj_list').css('height', $('#obj_list').height() - 32) + + $('.main').show() + $('#canvas1').attr('width', $('#canvas-div').width()) + $('#canvas2').attr('width', $('#canvas-div').width()) + var openAs = { + _name: '', + set name(val) { + if (val == '') { + $('title').html('Soon Design') + } else { + $('title').html('Soon Design - ' + val) + } + this._name = val + }, + get name() { + return this._name + } + } //被打开的文件名 + let objs1 = [] + let objs2 = [] + let step1 = { val: 0 }, + step2 = { val: 0 } + let step = step1 + let objs = objs1 + let recordJson1 = [], + recordJson2 = [], + recordJson = recordJson1, + recordObjs1 = [], + recordObjs2 = [], + recordObjs = recordObjs1 + let pre_objs1 = [], + pre_objs2 = [], + next_objs1 = [], + next_objs2 = [], + pre_objs = pre_objs1, + next_objs = next_objs1 + let pre_json1 = [], + pre_json2 = [], + next_json1 = [], + next_json2 = [], + pre_json = pre_json1, + next_json = next_json1 + var canvas1 = new fabric.Canvas('canvas1') + var canvas2 = new fabric.Canvas('canvas2') + fabric.Object.prototype.objectCaching = false + var ctx1 = canvas1.getSelectionContext() + var ctx2 = canvas2.getSelectionContext() + let is_bgi_add = false + canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) + //console.log(canvas1.width / 2, canvas1.height / 2); + //console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200); + canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) + zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200 + + // 初始化标尺 + initRulers(); + $('#source_front').width(zoom * $('#source_front').width()) + $('#source_back').width(zoom * $('#source_back').width()) + $('.canvas-container:eq(1)').hide() + var canvas = canvas1 + fabric.Object.prototype.set({ + borderColor: '#9013FE', + cornerColor: '#9013FE', //激活状态角落图标的填充颜色 + cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 1, + cornerSize: 6, + cornerStyle: 'circle', //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0 + }) + canvas1.preserveObjectStacking = true + canvas2.preserveObjectStacking = true + + var front_list = [] + ipcRenderer.on('font-list', (event, data) => { + const newFontList = [] + data.map((item) => { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')) + } else { + newFontList.push(item) + } + }) + front_list = newFontList + for (let item of front_list) { + $('#text_font_family').append("') + $("#circle_text_font_family").append(""); + $('#out_text_font_family').append("') + $('#barcode_font_family').append("') + $('#count_font_family').append("') + } + }) + ipcRenderer.on('sys-lan', (event, data) => { + let lang = localStorage.getItem('lang') + if (lang) { + s_lan = lang + langua_ge(lang) + $('#language_select').val(lang) + return + } + switch (data) { + case 'zh-CN': + s_lan = 'zh' + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh': + s_lan = 'zh' + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh-TW': + s_lan = 'ozh' + langua_ge('ozh') + $('#language_select').val('ozh') + localStorage.setItem('lang', 'ozh') + break + case 'en': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-AU': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-CA': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-GB': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-NZ': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-US': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-ZA': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + } + }) + ipcRenderer.on('close', (event, message) => { + layer.confirm( + '' + language_str('whetherSave') + '', + { + //是否保存当前文件? + btn: [language_str('save'), language_str('noSave'), language_str('cancel')], //['保存', '不保存', '取消'] + btn3: function (index, layero) { } + }, + function (index, layero) { + output(() => ipcRenderer.send('run-close')) + return + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + + ipcRenderer.send('run-close') + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + ipcRenderer.send('run-close') + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //保存成功至 + saveHistory() + ipcRenderer.send('run-close') + }) + .catch((err) => { + console.log() + }) + layer.close(index) + }, + function (index) { + ipcRenderer.send('run-close') + } + ) + }) + + addBackground() //添加背景 白色卡片 + + initAligningGuidelines(canvas1) + initAligningGuidelines(canvas2) + //canvas.setZoom(0.5) + //recordObjs1.push(JSON.stringify(objs1)); + //recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //recordObjs2.push(JSON.stringify(objs2)); + //recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + canvas.renderAll() //重新渲染画布 + var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100) + zoom = val / 100 + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } + }) + window.rotate = function (id) { + let angle = $(`#${id}`).attr('angle') - 0 + angle += 90 + $(`#${id}`).css('transform', `rotate(${angle}deg)`) + $(`#${id}`).attr('angle', angle) + } + $('#display').on('click', function () { + // 类型改变 + fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + + fabric.Image.fromURL('./public/images/back_bg_2.png', function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + fabric.Image.fromURL('./public/images/op_2.png', function (i3) { + i2.left = background_image.left + i2.top = background_image.top + let img3 = i3 + display_func(img1, img2, img3) + }) + }) + }) + }) + // 导出文件 + async function savePdf(buffer) { + let fs = require('fs'); + // 弹出保存对话框 + const { canceled, filePath } = await dialog.showSaveDialog({ + title: '保存文件', + defaultPath: path.join(exePath, 'output.pdf'), // 默认路径 + filters: [{ name: 'PDF 文件', extensions: ['pdf'] }] // 文件过滤器 + }); + + if (canceled) { + console.log('用户取消了保存操作'); + return; + } + + // 将Buffer写入PNG文件到用户选择的路径 + fs.writeFile(filePath, buffer, (err) => { + if (err) { + console.error('写入文件失败:', err); + } else { + layer.msg(language_str("saveSucc") + filePath);//'保存成功至' + console.log('PNG文件已保存至:', filePath); + } + }); + } + function display_func(img1, img2, img3) { + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $('#circle_text_control').hide() + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + let _objs1 = canvas1.getObjects() + let g1 = [] + let g3 = [] + let g5 = [] + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + + // g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let _objs2 = canvas2.getObjects() + let g2 = [] + let g4 = [] + let g6 = [] + left = 0 + top = 0 + black_top = 0 + black_left = 0 + all_top = 0 + all_left = 0 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //黑色字或计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if (idx != 0 && objs2[idx - 1].type == 10) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + // g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + let fs = require('fs'); + const Buffer = require('buffer').Buffer; + const buffer3 = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + const printPath3 = path.join(exePath, 'print3.png'); + fs.writeFileSync(printPath3, buffer3); + const buffer4 = Buffer.from(url4.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + const printPath4 = path.join(exePath, 'print4.png'); + fs.writeFileSync(printPath4, buffer4); + // 创建 PDF 实例 + const pdf = new jspdf.jsPDF({ + orientation: 'landscape', + unit: 'px', + format: [1012, 648] + }); + let btns = [language_str('output')] + if ((objs1.length != 0) & (objs2.length == 0)) { + //只有正面 + let img = new Image() + img.src = printPath3 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + btns.push('打印正面') + } else if ((objs1.length == 0) & (objs2.length != 0)) { + //只有背面 + let img = new Image() + img.src = printPath4 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + btns.push('打印背面') + } else { + let img = new Image() + img.src = printPath3 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + + pdf.addPage() + let img2 = new Image() + img2.src = printPath4 + pdf.addImage(img2, 'JPEG', 0, 0, 1012, 648) + btns.push('打印正面') + btns.push('打印背面') + } + + var pdfBlob = pdf.output('blob') + var pdfBuffer = pdf.output('arraybuffer') + + layer.open({ + type: 1, + area: [w, h], + title: language_str('display'), //不显示标题栏"预览" + shadeClose: true, //点击遮罩关闭 + content: ` +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
`, + btn: btns, //'导出' + btn1: function (index, layero) { + savePdf(Buffer.from(pdfBuffer)) + }, + btn2: function () { + if (btns[1] === '打印正面') { + printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } else { + printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } + }, + btn3: function () { + //打印背面 + printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } + }) + } + function output(callback = null, _save = save) { + // 类型改变 + fabric.Image.fromURL('./public/images/op_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + fabric.Image.fromURL('./public/images/op_2.png', function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + let _objs1 = canvas1.getObjects() + let g1 = [] + let g3 = [] + let g5 = [] //黑色图层 + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && item.get('fill') == '#000000') || + ( + objs1[idx - 1].type == 11 + && + item.get("fill") == "#000000" + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // || + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let _objs2 = canvas2.getObjects() + let g2 = [] + let g4 = [] + let g6 = [] //黑色图层 + left = 0 + top = 0 + black_left = 0 //黑色左裁剪 + black_top = 0 //黑色顶裁剪 + all_left = 0 //黑色左裁剪 + all_top = 0 //黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } + if ((objs1.length != 0) & (objs2.length == 0)) { + op.flag = 2 + } else if ((objs1.length == 0) & (objs2.length != 0)) { + op.flag = 3 + } + op.frontColorPic = desEncrypt(url1) + op.backColorPic = desEncrypt(url2) + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = desEncrypt(url5) + op.backBlackPic = desEncrypt(url6) + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + if (idx == 0) continue //background-image + let type = objs1[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 1, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 3, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontBackgroudColor: item.get('textBackgroundColor'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + Exceed: objs1[idx - 1].exceed, + AutoWrap: objs1[idx - 1].autoWrap, + FixSize: objs1[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs1[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 4, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].color, + DefaultText: objs1[idx - 1].val, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 5, + name: objs1[idx - 1].name, + FontSize: parseInt(objs1[idx - 1].fontSize), + DefaultText: objs1[idx - 1].val, + HideText: objs1[idx - 1].show, + FontFamily: objs1[idx - 1].font, + FontColor: objs1[idx - 1].color, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 6, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: objs1[idx - 1].start, + add: parseInt(objs1[idx - 1].add), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (idx == 0) continue //background-image + let type = objs2[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 1, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 3, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontSize: parseInt(item.get('fontSize')), + FontBackgroudColor: item.get('textBackgroundColor'), + FontColor: item.get('fill'), + Exceed: objs2[idx - 1].exceed, + AutoWrap: objs2[idx - 1].autoWrap, + FixSize: objs2[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs2[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 4, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].color, + DefaultText: objs2[idx - 1].val, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 5, + name: objs2[idx - 1].name, + DefaultText: objs2[idx - 1].val, + HideText: objs1[idx - 1].show, + FontSize: parseInt(objs2[idx - 1].fontSize), + FontFamily: objs2[idx - 1].font, + FontColor: objs2[idx - 1].color, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 6, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: parseInt(objs2[idx - 1].start), + add: parseInt(objs2[idx - 1].add), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + _save(op, callback) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + } + $('#output').on('click', function () { + output(null, saveAs) + return + // 类型改变 + fabric.Image.fromURL('./public/images/op_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + fabric.Image.fromURL('./public/images/op_2.png', function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + let _objs1 = canvas1.getObjects() + let g1 = [] + let g3 = [] + let g5 = [] //黑色图层 + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let _objs2 = canvas2.getObjects() + let g2 = [] + let g4 = [] + let g6 = [] //黑色图层 + left = 0 + top = 0 + black_left = 0 //黑色左裁剪 + black_top = 0 //黑色顶裁剪 + all_left = 0 //黑色左裁剪 + all_top = 0 //黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } + if ((objs1.length != 0) & (objs2.length == 0)) { + op.flag = 2 + } else if ((objs1.length == 0) & (objs2.length != 0)) { + op.flag = 3 + } + op.frontColorPic = desEncrypt(url1) + op.backColorPic = desEncrypt(url2) + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = desEncrypt(url5) + op.backBlackPic = desEncrypt(url6) + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + if (idx == 0) continue //background-image + let type = objs1[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 1, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 3, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontBackgroudColor: item.get('textBackgroundColor'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + Exceed: objs1[idx - 1].exceed, + AutoWrap: objs1[idx - 1].autoWrap, + FixSize: objs1[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs1[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 4, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].color, + DefaultText: objs1[idx - 1].val, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 5, + name: objs1[idx - 1].name, + FontSize: parseInt(objs1[idx - 1].fontSize), + DefaultText: objs1[idx - 1].val, + HideText: objs1[idx - 1].show, + FontFamily: objs1[idx - 1].font, + FontColor: objs1[idx - 1].color, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 6, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: objs1[idx - 1].start, + add: parseInt(objs1[idx - 1].add), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (idx == 0) continue //background-image + let type = objs2[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 1, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 3, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontSize: parseInt(item.get('fontSize')), + FontBackgroudColor: item.get('textBackgroundColor'), + FontColor: item.get('fill'), + Exceed: objs2[idx - 1].exceed, + AutoWrap: objs2[idx - 1].autoWrap, + FixSize: objs2[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs2[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 4, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].color, + DefaultText: objs2[idx - 1].val, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 5, + name: objs2[idx - 1].name, + DefaultText: objs2[idx - 1].val, + HideText: objs1[idx - 1].show, + FontSize: parseInt(objs2[idx - 1].fontSize), + FontFamily: objs2[idx - 1].font, + FontColor: objs2[idx - 1].color, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 6, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: parseInt(objs2[idx - 1].start), + add: parseInt(objs2[idx - 1].add), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'CS File Type', extensions: ['cs'] }] + }) + .then((result) => { + if (result.canceled) return + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.cs' + } + let fs = require('fs') + op.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(op)) + }) + .catch((err) => { + console.log() + }) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + }) + var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false + $('input').focus(function () { + console.log(1) + _inputEdit = true + }) + $('input').blur(function () { + _inputEdit = false + }) + $('textarea').focus(function () { + console.log(1) + _inputEdit = true + }) + $('textarea').blur(function () { + _inputEdit = false + }) + $('body').keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false + } + if (event.keyCode == 67) { + _keyc = false + } + }) + $('body').keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $('#new').click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $('#open').click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $('#save').click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + output(null, saveAs); + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $('#previous').click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $('#next').click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + pasteFromClipboard(); + return; + } + // CTRL/CMD+SHIFT+DEL - 清空 + if (isModifierKey && event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#clean').click(); + return; + } + // CTRL/CMD+DEL - 删除 + if (isModifierKey && !event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#delete').click(); + return; + } + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 67) { + _keyc = true + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 37) { + //left + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + if (event.keyCode == 38) { + //up + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + if (event.keyCode == 39) { + //right + let v = 1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + if (event.keyCode == 40) { + //down + let v = +1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + updateList() + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + handleObjectSelected() + } + } + }) + let pre_add_image + $('#addPic').click(function () { + OpenDialog() + function OpenDialog() { + dialog + .showOpenDialog({ + title: language_str('selectPic'), //请选择图片 + buttonLabel: language_str('comf'), //"确认" + filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] + }) + .then((result) => { + if (result.canceled) return + //console.log(result.filePaths) + fabric.Image.fromURL(result.filePaths[0], function (image) { + //image.left = pointer.x; + //image.top = pointer.y; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 1 + }) + //canvas.add(image); + //objs.push({type: 1}); + //canvas.renderAll(); + //updateList(); + //recordState(); + }) + }) + .catch((err) => { + console.log(err) + }) + } + }) + $('#addOutPic').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 2 + }) + $('#addText').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 3 + }) + $("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; + }); + $('#addOutText').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 4 + }) + $('#addRect').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 5 + }) + $('#addCircle').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 6 + }) + $('#addLine').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 7 + }) + $('#addQrCode').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 8 + }) + $('#addBarCode').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 9 + }) + $('#addCounter').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + addState = 10 + }) + $('#version_change').click(function () { + if (bg_version == 1) { + bg_version = 2 + } else { + bg_version = 1 + } + // 类型改变 + $('#source_front').attr('src', './public/images/front_bg' + bg_version + '_2.png') + canvas.renderAll() + }) + $('#front_side').click(function () { + $(this).addClass('ui-button-active') + $('#back_side').removeClass('ui-button-active') + $('.canvas-container:eq(0)').show() + $('.canvas-container:eq(1)').hide() + console.log('------') + console.log(recordObjs1) + console.log('------') + canvas = canvas1 + objs = objs1 + background_image = background_image1 + next_objs = next_objs1 + pre_objs = pre_objs1 + next_json = next_json1 + pre_json = pre_json1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + updateList() + + $('#line_control').hide() + $('#base_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //背景 + }) + $('#back_side').click(function () { + $(this).addClass('ui-button-active') + $('#front_side').removeClass('ui-button-active') + $('.canvas-container:eq(0)').hide() + $('.canvas-container:eq(1)').show() + canvas = canvas2 + objs = objs2 + background_image = background_image2 + next_objs = next_objs2 + pre_objs = pre_objs2 + next_json = next_json2 + pre_json = pre_json2 + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + updateList() + $('#line_control').hide() + $('#base_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + }) + $('#top_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set('top', background_image.get('top')) + canvas.renderAll() + return + } + let top = 0 + for (let item of _objs) { + let t_top = item.get('top') + if (t_top < top) { + top = t_top + } + } + groupItems = [] + for (let item of _objs) { + item.set('top', top) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#bottom_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get('strokeWidth') + } + _objs[0].set('top', background_image.get('top') + background_image.get('height') - _objs[0].get('height') * _objs[0].get('scaleY') - other) + canvas.renderAll() + return + } + let bottom = 0 + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + let t_b = item.get('top') + item.get('height') * item.get('scaleY') + other + if (t_b > bottom) { + bottom = t_b + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('top', bottom - item.get('height') * item.get('scaleY') - other) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#center_y_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get('strokeWidth') + } + _objs[0].set('top', background_image.get('top') + (background_image.get('height') - (_objs[0].get('height') + other) * _objs[0].get('scaleY')) / 2) + canvas.renderAll() + return + } + let top = 0 + for (let item of _objs) { + let t_top = item.get('top') + if (t_top < top) { + top = t_top + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('top', top + (item.group.height - (item.get('height') + other) * item.get('scaleY')) / 2) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#left_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set('left', background_image.get('left')) + canvas.renderAll() + return + } + let left = 0 + for (let item of _objs) { + let t_left = item.get('left') + if (t_left < left) { + left = t_left + } + } + groupItems = [] + for (let item of _objs) { + item.set('left', left) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#right_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + _objs[0].set('left', background_image.get('left') + background_image.get('width') - _objs[0].get('width') * _objs[0].get('scaleX') - other) + canvas.renderAll() + return + } + let right = 0 + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + let t_r = item.get('left') + item.get('width') * item.get('scaleX') + other + if (t_r > right) { + right = t_r + } + } + let groupItems = [] + + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('left', right - item.get('width') * item.get('scaleX') - other) + groupItems.push(item) + } + //BUG1 + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#center_x_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get('strokeWidth') + } + _objs[0].set('left', background_image.get('left') + (background_image.get('width') - (_objs[0].get('width') + other) * _objs[0].get('scaleX')) / 2) + canvas.renderAll() + return + } + let left = 0 + for (let item of _objs) { + let t_left = item.get('left') + if (t_left < left) { + left = t_left + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('left', left + (item.group.width - (item.get('width') + other) * item.get('scaleX')) / 2) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#up_obj').click(function () { + //上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()) + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()) + canvas.renderAll() + let temp = objs[idx - 1] + objs[idx - 1] = objs[idx] + objs[idx] = temp + //console.log(objs); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + recordState() + updateList() + selectObj(idx) + } + }) + $('#down_obj').click(function () { + //下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()) + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()) + canvas.renderAll() + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1] + objs[idx - 1] = objs[idx - 2] + objs[idx - 2] = temp + recordState() + updateList() + selectObj(idx - 2) + } + }) + $('#set_bg').click(function () { + //设置为背景 + let obj = canvas.getActiveObject() + let idx = getIndex(canvas.getActiveObject()) + const scaleX = 1012 / obj.width + const scaleY = 648 / obj.height + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - 1012) / 2, + top: ($('#canvas1').height() - 648) / 2 + }) + $('#text_w').val(1012) + $('#text_h').val(648) + $('#text_x').val(0) + $('#text_y').val(0) + canvas.sendToBack(canvas.getActiveObject()) + canvas.bringForward(canvas.getActiveObject()) + canvas.renderAll() + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + recordState() + updateList() + selectObj(0) + }) + $('#previous').click(function () { + //撤销一次操作 + if (step.val == 0) { + return + } + + step.val-- + console.log(recordJson[step.val]) + canvas.loadFromJSON(recordJson[step.val]) + if ($('#front_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs1 = JSON.parse(recordObjs[step.val]) + objs = objs1 + } else if ($('#back_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs2 = JSON.parse(recordObjs[step.val]) + objs = objs2 + } + + background_image = canvas.getObjects()[0] + canvas.renderAll() + //recordState(); + updateList() + + selectObj(-1) + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + }) + $('#next').click(function () { + //撤销一次撤销操作 + if (step.val >= recordObjs.length - 1) { + return + } + + ++step.val + canvas.loadFromJSON(recordJson[step.val]) + if ($('#front_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs1 = JSON.parse(recordObjs[step.val]) + objs = objs1 + } else if ($('#back_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs2 = JSON.parse(recordObjs[step.val]) + objs = objs2 + } + background_image = canvas.getObjects()[0] + canvas.renderAll() + selectObj(-1) + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + updateList() + }) + $('#clean').click(function () { + objs.splice(0, objs.length) + + let allObjs = canvas.getObjects() + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item) + } + } + updateList() + recordState() + }) + $('#rotate').click(function () { + let obj = canvas.getActiveObject() + obj.rotate(Math.round((obj.get('angle') + 90) % 360)) + $('#text_r').val(obj.get('angle')) + canvas.renderAll() + updateControls() + recordState() + }) + $('#text_italics').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('fontStyle', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontStyle', 'italic') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#text_bold').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选,恢复正常 + canvas.getActiveObject().set('fontWeight', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontWeight', 'bold') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#text_underline').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('underline', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('underline', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#text_linethrough').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('linethrough', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('linethrough', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + recordState() + canvas.renderAll() + }) + $('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + recordState() + canvas.renderAll() + }) + $('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + recordState() + canvas.renderAll() + }) + $('#out_text_italics').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('fontStyle', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontStyle', 'italic') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#out_text_bold').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选,恢复正常 + canvas.getActiveObject().set('fontWeight', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontWeight', 'bold') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#out_text_underline').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('underline', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('underline', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#out_text_linethrough').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('linethrough', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('linethrough', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#lock').click(function () { + canvas.getActiveObject().set('selectable', !canvas.getActiveObject().get('selectable')) + if (canvas.getActiveObject().get('selectable')) { + $('#lock_img').attr('src', 'public/images/unlock20.png') //已经上锁,需要解锁 + } else { + $('#lock_img').attr('src', 'public/images/lock20.png') //没上锁,可以上锁 + } + recordState() + canvas.renderAll() + }) + $('#text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#text_back_color').val('#ffffff') + canvas.renderAll() + }) + $('#out_text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#out_text_back_color').val('#ffffff') + canvas.renderAll() + }) + $('#rect_transparent').click(function () { + canvas.getActiveObject().set('fill', '') + $('#rect_color').val('#ffffff') + canvas.renderAll() + }) + $('#rect_stroke_transparent').click(function () { + canvas.getActiveObject().set('stroke', '') + $('#rect_stroke').val('#ffffff') + canvas.renderAll() + }) + $('#circle_transparent').click(function () { + canvas.getActiveObject().set('fill', '') + $('#circle_color').val('#ffffff') + canvas.renderAll() + }) + $('#circle_stroke_transparent').click(function () { + canvas.getActiveObject().set('stroke', '') + $('#circle_stroke').val('#ffffff') + canvas.renderAll() + }) + function saveAs(op1, callback) { + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + let con_o = Object.assign(o, op1) + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + if (callback && typeof callback === 'function') { + callback() + } + }) + .catch((err) => { + console.log() + }) + } + function save(op1, callback) { + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + let con_o = Object.assign(o, op1) + console.log(op1) + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(con_o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + if (callback && typeof callback === 'function') { + callback() + } + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + if (callback && typeof callback === 'function') { + callback() + } + }) + .catch((err) => { + console.log() + }) + } + // 复制 + $("#copy").click(function () { + copySelection(); + }) + // 粘贴 + $("#paste").click(function () { + pasteFromClipboard(); + }) + // 保存 + $('#save').click(function () { + output() + return + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + let con_o = Object.assign(o, op1) + console.log(op1) + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(con_o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + }) + .catch((err) => { + console.log() + }) + }) + function getDate() { + let date = new Date() + return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + } + function getAbsoluteXY(_obj) { + let coord = _obj.get('lineCoords') + //let xs = [], ys = []; + //for(let item in coord){ + // xs.push(coord[item].x); + // ys.push(coord[item].y); + //} + //xs.sort(function(a,b){return a-b}); + //ys.sort(function(a,b){return a-b}); + //return [xs[0], ys[0]]; + return [_obj.get('left'), _obj.get('top')] + } + let fullPath = path.join(exePath, 'data.json') + function saveHistory() { + let fs = require('fs') + let j = {} + try { + fsData = fs.readFileSync(fullPath) + j = JSON.parse(fsData.toString()) + for (let item of j.history) { + if (item.path == openAs.name) { + item.time = getDate() + fs.writeFileSync(fullPath, JSON.stringify(j)) + return + } + } + j.history.push({ time: getDate(), path: openAs.name }) + fs.writeFileSync(fullPath, JSON.stringify(j)) + } catch (e) { + let j = { history: { time: getDate(), path: openAs.name } } + fs.writeFileSync(fullPath, JSON.stringify(j)) + } + } + function open(file) { + let fs = require('fs') + var fsData = fs.readFileSync(file) + openAs.name = file + let j = JSON.parse(fsData.toString()) + let left = background_image.left + let top = background_image.top + let new_left = j.f.objects[0].left + let new_top = j.f.objects[0].top + for (let o of j.f.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + for (let o of j.b.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + canvas1.loadFromJSON(j.f) + canvas2.loadFromJSON(j.b) + background_image1 = canvas1.getObjects()[0] + background_image2 = canvas2.getObjects()[0] + + objs2 = j.bo + objs1 = j.fo + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + background_image = background_image1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + background_image = background_image2 + step = step2 + } + canvas1.renderAll() + canvas2.renderAll() + updateList() /* + 修复打开新文件后无法撤销的bug + 2022-07-16 + */ + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j1.objects[0].hoverCursor = 'default' + recordJson1.push(j1) + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j2.objects[0].hoverCursor = 'default' + recordJson2.push(j2) + }, 0) + } + $('#open').click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm( + '' + language_str('whetherSave') + '', + function (index) { + //confirm 是否保存当前文件? + //let j = canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + //let b = canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + //let o = { f : j, b : b,fo : objs1, bo : objs2}; + //if(openAs != ""){ + // //打开的文件 + // let fs = require('fs') + // fs.writeFileSync(openAs,JSON.stringify(o)) + // layer.msg('保存成功至'+ openAs); + // + // OpenDialog();return; + //} + //dialog.showSaveDialog({ + // title:'保存文件', + // filters: [ + // { name: 'FCS File Type', extensions: ['fcs'] }, + // ] + //}).then(result=>{ + // let fs = require('fs') + // fs.writeFileSync(result.filePath,JSON.stringify(o)) + // + // OpenDialog(); + //}).catch(err=>{ + // console.log() + //}) + /* + let j = canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + let b = canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for( let _o of canvas1.getObjects() ){ + if( getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left){ + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if( getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top){ + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top:top/zoom, + left:left/zoom + }); + left = 0, top = 0; + for( let _o of canvas2.getObjects() ){ + if( getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left){ + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if( getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top){ + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top:top/zoom, + left:left/zoom + }); + let o = { f : j, b : b,fo : objs1, bo : objs2,fp:fp,bp:bp}; + if(openAs.name != ""){ + //打开的文件 + let fs = require('fs') + fs.writeFileSync(openAs.name,JSON.stringify(o)) + layer.msg(language_str("saveSucc")+ openAs.name);//'保存成功至' + saveHistory(); + + OpenDialog(); + return; + } + dialog.showSaveDialog({ + title:language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result=>{ + if(result.canceled)return; + if(result.filePath.substring(result.filePath.length-5).indexOf('.') == -1){ + result.filePath += '.soon'; + } + let fs = require('fs') + fs.writeFileSync(result.filePath,JSON.stringify(o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc")+ openAs.name);//'保存成功至' + saveHistory(); + OpenDialog(); + }).catch(err=>{ + console.log() + }) + */ + output(OpenDialog) + layer.close(index) + }, + function (index) { + OpenDialog() + } + ) + function OpenDialog() { + dialog + .showOpenDialog({ + title: '请选择文件', //confirm + buttonLabel: language_str('comf'), //"确认" + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.canceled) return + //console.log(result.filePaths) + let fs = require('fs') + var fsData = fs.readFileSync(result.filePaths[0]) + openAs.name = result.filePaths[0] + let j = JSON.parse(fsData.toString()) + let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + if (type == 1) { + ipcRenderer.send('open-design-page', openAs.name, type); + return + } + let left = background_image.left + let top = background_image.top + let new_left = j.f.objects[0].left + let new_top = j.f.objects[0].top + for (let o of j.f.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + for (let o of j.b.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + canvas1.loadFromJSON(j.f) + canvas2.loadFromJSON(j.b) + background_image1 = canvas1.getObjects()[0] + background_image2 = canvas2.getObjects()[0] + + objs2 = j.bo + objs1 = j.fo + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + background_image = background_image1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + background_image = background_image2 + step = step2 + } + canvas1.renderAll() + canvas2.renderAll() + updateList() + /* + 修复打开新文件后无法撤销的bug + 2022-07-16 + */ + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j1.objects[0].hoverCursor = 'default' + recordJson1.push(j1) + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j2.objects[0].hoverCursor = 'default' + recordJson2.push(j2) + }, 0) + }) + .catch((err) => { + console.log(err) + }) + } + }) + function clearAll() { + //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; + //let j = JSON.parse(blank); + //canvas1.setZoom(1); + //canvas2.setZoom(1); + //canvas1.loadFromJSON(j.f); + //canvas2.loadFromJSON(j.b); + let t_objs1 = canvas1.getObjects() + let t_objs2 = canvas2.getObjects() + for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]) + } + for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]) + } + objs2 = [] + objs1 = [] + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + } + openAs.name = '' + canvas1.renderAll() + canvas2.renderAll() + updateList() + console.log() + } + $('#new').click(function () { + if (step.val == 0) { + ipcRenderer.send('open-first-page'); + return; + } + layer.confirm( + '' + language_str('whetherSave') + '', + function (index) { + //confirm 是否保存当前文件? + output(() => ipcRenderer.send('open-first-page')) + return + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page') + //saveHistory(); + //clearAll(); + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page') + //saveHistory(); + //clearAll(); + }) + .catch((err) => { + console.log() + }) + layer.close(index) + }, + function (index) { + /* + 2022-07-16 直接跳转至首页 + */ + //clearAll(); + ipcRenderer.send('open-first-page') + } + ) + }) + function chagneTitle() { } + function addBackground() { + // 类型改变 + fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { + //image.set("scaleX",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) + //image.set("scaleY",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) + + image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 + image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + image.setSrc(image.toDataURL(), function (image) { + background_image1 = image + background_image = background_image1 + canvas.add(image) + canvas.renderAll() + //pre_objs1.push(JSON.stringify(objs1)); + //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs1.push(JSON.stringify(objs1)) + recordJson1.push(canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) + }) + // 类型改变 + fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { + image.left = ($('#canvas2').width() - image.width) / 2 + image.top = ($('#canvas2').height() - image.height) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image + canvas2.add(image) + canvas2.renderAll() + //pre_objs2.push(JSON.stringify(objs2)); + //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs2.push(JSON.stringify(objs2)) + recordJson2.push(canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) + + let file = GetFile().get('file') + console.log(file) + if (file && file != 'empty') { + open(file) + } + }) + }) + is_bgi_add = true + }) + } + function selectObject(target, index) { + updateControls() + + $('#base_control').show() + //$("#text_x").val((target.left - background_image.left).toFixed(1)); + //$("#text_y").val((target.top - background_image.top).toFixed(1)); + //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); + //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); + //$("#text_r").val(target.get("angle").toFixed(0)); + } + function updateControls() { + $('#text_r').val(canvas.getActiveObject().get('angle').toFixed(0)) + $('#text_x').val((canvas.getActiveObject().get('left') - background_image.left).toFixed(0)) + $('#text_y').val((canvas.getActiveObject().get('top') - background_image.top).toFixed(0)) + $('#text_h').val((canvas.getActiveObject().get('scaleY') * canvas.getActiveObject().get('height')).toFixed(0)) + $('#text_w').val((canvas.getActiveObject().get('scaleX') * canvas.getActiveObject().get('width')).toFixed(0)) + if (canvas.getActiveObject().get('selectable')) { + $('#lock_img').attr('src', 'public/images/unlock20.png') //已经上锁,需要解锁 + } else { + $('#lock_img').attr('src', 'public/images/lock20.png') //没上锁,可以上锁 + } + } + function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false + } + } + for (let item of objs2) { + if (item.name == name) { + return false + } + } + return true + } + function resName(pre_name) { + //返回的是不重复的字段名 + let num = objs1.length + objs2.length - 1 + do { + num++ + } while (!checkName(pre_name + num)) //重复了就再加1 + return pre_name + num + } + function addPic(pointer) { + pre_add_image.left = pointer.x + pre_add_image.top = pointer.y + + canvas.add(pre_add_image) + + objs.push({ type: 1, black: false }) + canvas.renderAll() + updateList() + recordState() + //OpenDialog(); + //function OpenDialog() + //{ + // dialog.showOpenDialog({ + // title: "请选择图片", + // buttonLabel: "确认", + // filters: [ + // { name: 'Image File Type', extensions: ['png', 'jpg'] }, + // ] + // }).then(result => { + // if(result.canceled)return; + // //console.log(result.filePaths) + // fabric.Image.fromURL(result.filePaths[0], function(image) { + // image.left = pointer.x; + // image.top = pointer.y; + // + // canvas.add(image); + // objs.push({type: 1}); + // canvas.renderAll(); + // updateList(); + // recordState(); + // } ); + // }).catch(err => { + // console.log(err) + // }) + //} + } + function selectPic(target, index) { + //透明度 + + $('#pic_control').show() + $("#set_bg").show(); + pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + + let idx = getIndex(canvas.getActiveObject()) + // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addOutPic(pointer) { + fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + let width = canvas.getWidth() + let height = canvas.getHeight() + image.left = pointer.x + image.top = pointer.y + image.opacity = 1 + //image.on("selected",function(){//test well + // console.log(222); + //}); + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image) + objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) + canvas.renderAll() + updateList() + recordState() + }) + }) + } + function selectOutPic(target, index) { + //字段名 + $('#out_pic_control').show() + out_pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + let idx = getIndex(canvas.getActiveObject()) + $('#out_pic_field_name').val(objs[idx - 1].name) + // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + + function addText(pointer) { + let text = new fabric.IText('TEXT', { + //源:文本 + left: pointer.x, + top: pointer.y, + fill: '#000000', + textBackgroundColor: '', + fontStyle: 'normal', + fontWeight: 'normal', + underline: false, + linethrough: false, + fontFamily: 'Arial' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + canvas.add(text) + objs.push({ type: 3, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectText() { + //console.log(canvas.getActiveObject().get("fontFamily")); + $('#text_control').show() + $('#text_color').val(canvas.getActiveObject().get('fill')) + $('#text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#text_size").val(val); + $('#text_text').val(canvas.getActiveObject().get('text')) + $('#text_font_family').val(canvas.getActiveObject().get('fontFamily')) + + if (canvas.getActiveObject().get('underline') == true) { + $('#text_underline').addClass('ui-radio-acitve') + } else { + $('#text_underline').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontWeight') == 'bold') { + $('#text_bold').addClass('ui-radio-acitve') + } else { + $('#text_bold').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontStyle') == 'italic') { + $('#text_italics').addClass('ui-radio-acitve') + } else { + $('#text_italics').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('linethrough') == true) { + $('#text_linethrough').addClass('ui-radio-acitve') + } else { + $('#text_linethrough').removeClass('ui-radio-acitve') + } + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; + } + function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180/Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg + } + function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width/2,canvas.height/2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance*2, + fontSize: 30, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height/2 ? true : false + }); + // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) + // text.set({diameter: distance*2}) + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false, circleCenter: 0 }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectCircleText() { + console.log(canvas.getActiveObject().get("text")) + let idx = getIndex(canvas.getActiveObject()); + $("#circle_text_control").show(); + $("#circle_text_color").val(canvas.getActiveObject().get("fill")); + $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); + circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")-50); + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(canvas.getActiveObject().get("text")); + $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function addOutText(pointer) { + //MERGE_TEXT_1 + let text = new fabric.Text('MERGE TEXT', { + //源:合成文本 + left: pointer.x, + top: pointer.y, + fill: '#000000', + fontStyle: 'normal', + fontWeight: 'normal', + textBackgroundColor: '', + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }) + + //text.on({ + // 'scaling': function(e) { + // var obj = this, + // w = obj.width * obj.scaleX, + // h = obj.height * obj.scaleY; + // + // obj.set({ + // 'height' : h, + // 'width' : w, + // 'scaleX' : 1, + // 'scaleY' : 1 + // }); + // } + //}); + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + + canvas.add(text) + objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) + canvas.renderAll() + updateList() + recordState() + } + function selectOutText() { + //color + //size + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $('#out_text_control').show() + $('#out_text_color').val(canvas.getActiveObject().get('fill')) + $('#out_text_text').val(canvas.getActiveObject().get('text')) + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#out_text_size").val(val); + $('#out_text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) + let idx = getIndex(canvas.getActiveObject()) + $('#out_text_field_name').val(objs[idx - 1].name) + $('#out_text_font_family').val(canvas.getActiveObject().get('fontFamily')) + $('#exceed').prop('checked', objs[idx - 1].exceed ? 'checked' : '') + $('#autoWrap').prop('checked', objs[idx - 1].autoWrap ? 'checked' : '') + $('#fixSize').prop('checked', objs[idx - 1].fixSize ? 'checked' : '') + if (canvas.getActiveObject().get('underline') == true) { + $('#out_text_underline').addClass('ui-radio-acitve') + } else { + $('#out_text_underline').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontWeight') == 'bold') { + $('#out_text_bold').addClass('ui-radio-acitve') + } else { + $('#out_text_bold').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontStyle') == 'italic') { + $('#out_text_italics').addClass('ui-radio-acitve') + } else { + $('#out_text_italics').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('linethrough') == true) { + $('#out_text_linethrough').addClass('ui-radio-acitve') + } else { + $('#out_text_linethrough').removeClass('ui-radio-acitve') + } + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 3, + width: 100, + height: 100 + }) + canvas.add(rect) + objs.push({ type: 5, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectRect() { + //color + $('#rect_control').show() + $('#rect_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) + $('#rect_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) + $('#rect_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) + $('#rect_radius').val(canvas.getActiveObject().get('rx')) + rect_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + let a = canvas.getActiveObject().getCoords() + console.log(getCoordsMinX(a)) + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 3, + radius: 100, + strokeUniform: true + }) + canvas.add(circle) + objs.push({ type: 6, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectCircle() { + //border-color + //color + $('#circle_control').show() + $('#circle_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) + $('#circle_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) + $('#circle_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) + circle_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addLine(pointer) { + let line = new fabric.Line([50, 100, 200, 100], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 3, + strokeDashArray: [0, 0], + }) + + line.setControlVisible('tl', false) + line.setControlVisible('tr', false) + line.setControlVisible('br', false) + line.setControlVisible('bl', false) + line.setControlVisible('mt', false) + line.setControlVisible('mb', false) + canvas.add(line) + objs.push({ type: 7, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectLine() { + //color + //weight + $('#line_control').show() + + let idx = getIndex(canvas.getActiveObject()) + $('#line_color').val(canvas.getActiveObject().get('stroke')) + $('#line_dist').val(canvas.getActiveObject().get('strokeDashArray')[0]) + $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addQrCode(pointer) { + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x + image.top = pointer.y + canvas.add(image) + objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) + canvas.renderAll() + updateList() + recordState() + }) + } + function selectQrCode() { + //字段名 + //background-color + //color + $('#qrcode_Controll').show() + let idx = getIndex(canvas.getActiveObject()) + $('#qrcode_field_name').val(objs[idx - 1].name) + $('#qrcode_color').val(objs[idx - 1].color) + $('#qrcode_val').val(objs[idx - 1].val) + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#barcode_controll').hide() + } + function addBarCode(pointer) { + JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x + image.top = pointer.y + + canvas.add(image) + objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) //show是取消文字显示 + canvas.renderAll() + updateList() + recordState() + }) + } + function selectBarCode() { + //字段名 + //background-color + //color + $('#barcode_controll').show() + let idx = getIndex(canvas.getActiveObject()) + $('#barcode_field_name').val(objs[idx - 1].name) + $('#barcode_font_family').val(objs[idx - 1].font) + let val = parseFloat(objs[idx - 1].fontSize)*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#barcode_size").val(val); + $('#barcode_val').val(objs[idx - 1].val) + $('#barcode_show').prop('checked', objs[idx - 1].show ? 'checked' : '') + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + } + function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fill: '#000000' + }) + canvas.add(text) + objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) + canvas.renderAll() + updateList() + recordState() + console.log(canvas.getObjects()) + } + function selectCounter() { + //起始 + //终止 + //增量 + $('#counter_control').show() + + let idx = getIndex(canvas.getActiveObject()) + $('#counter_start').val(objs[idx - 1].start) + $('#counter_add').val(objs[idx - 1].add) + $('#counter_color').val(canvas.getActiveObject().get('fill')) + $('#count_font_family').val(canvas.getActiveObject().get('fontFamily')) + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#count_size").val(val); + + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function getIndex(target, _canvas = null) { + let temp_objs + if (_canvas == null) { + temp_objs = canvas.getObjects() + } else { + temp_objs = _canvas.getObjects() + } + + let i = 0 + for (let res of temp_objs) { + if (target == res) { + return i + } + i++ + } + return 0 + } + function getType(target) { + let temp_objs = canvas.getObjects() + let i = 0 + for (let res of temp_objs) { + if (target == res) { + return objs[i].type + } + i++ + } + } + + function updateList() { + let str = '' + let i = 0 + for (let item of objs) { + switch (item.type) { + case 1 /* 2022-06-14修改*/: + str = '
IMG_" + i + '
' + str + break + case 2: + str = '
" + item.name + '
' + str + break + case 3: + str = '
" + 'TEXT_' + i + '
' + str + break + case 4: + str = '
" + item.name + '
' + str + break + case 5: + str = '
RECT_" + i + '
' + str + break + case 6: + str = '
CIRCLE_" + i + '
' + str + break + case 7: + str = '
LINE_" + i + '
' + str + break + case 8: + str = '
QRCODE_" + i + '
' + str + break + case 9: + str = '
BARCODE_" + i + '
' + str + break + case 10: + str = '
COUNTER_" + i + '
' + str + break + case 11: + str = "
" + "CIRCLE_TEXT_" + i + "
" + str; + break; + } + i++ + } + $('.obj_list').html(str) + } + function selectObj(i, notSelect = false) { + //for(let item in $(".obj_list").children()){ + // if(_i == i){ + // item.css("background-color","#6F7A84"); + // }else{ + // item.css("background-color",""); + // } + // _i++; + //} + $('.obj_list div').each(function (index) { + //if(index == i){ + if ($(this).children('img').attr('id') == 'obj' + i) { + $(this).css('background-color', '#6F7A84') + canvas.setActiveObject(canvas.getObjects()[i + 1]) + canvas.renderAll() + } else { + $(this).css('background-color', '') + } + }) + if (!notSelect) { + handleObjectSelected() + } + } + function recordState() { + //pre_objs.push(JSON.stringify(objs)); + //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //next_json = []; + //next_objs = []; + //recordObjs.push(JSON.stringify(objs)); + //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + ++step.val + for (let i = step.val; i < recordObjs.length; i++) { + recordObjs.pop() + recordJson.pop() + } + //recordObjs.splice(step.val, 1, JSON.stringify(objs)) + //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) + recordObjs.push(JSON.stringify(objs)) + j = canvas.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j.objects[0].hoverCursor = 'default' + recordJson.push(j) + console.log(recordJson) + } + function getCoordsMinX(acoords) { + x = acoords[0].x + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x + } + function getCoordsMaxX(acoords) { + x = acoords[0].x + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x + } + function getCoordsMinY(acoords) { + y = acoords[0].y + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y + } + function getCoordsMaxY(acoords) { + y = acoords[0].y + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y + } + + $('#text_y').bind('input propertychange', function () { + if ($('#text_y').val() == '') { + $('#text_y').val(0) + } + let val = background_image.top + if ($('#text_y').val() != '') { + val = background_image.top + parseInt($('#text_y').val()) + } + canvas.getActiveObject().set('top', val) + canvas.renderAll() + recordState() + }) + $('#text_x').bind('input propertychange', function () { + if ($('#text_x').val() == '') { + $('#text_x').val(0) + } + let val = background_image.left + if ($('#text_x').val() != '') { + val = background_image.left + parseInt($('#text_x').val()) + } + canvas.getActiveObject().set('left', val) + canvas.renderAll() + recordState() + }) + $('#text_w').bind('input propertychange', function () { + let val = '0' + if ($('#text_w').val() != '') { + val = $('#text_w').val() + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type + if (type == 3 || type == 4) { + canvas.getActiveObject().set('width', parseInt(val)) + } else { + canvas.getActiveObject().set('scaleX', parseInt(val) / canvas.getActiveObject().get('width')) + } + + canvas.renderAll() + recordState() + }) + $('#text_h').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + if (objs[idx - 1].type == 7) { + if ($('#text_h').val() == '') { + $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#text_h').val())) + canvas.renderAll() + return + } + let val = '0' + if ($('#text_h').val() != '') { + val = $('#text_h').val() + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type + if (type == 3 || type == 4) { + canvas.getActiveObject().set('height', parseInt(val)) + } else { + canvas.getActiveObject().set('scaleY', parseInt(val) / canvas.getActiveObject().get('height')) + } + + canvas.renderAll() + recordState() + }) + $('#text_r').bind('input propertychange', function () { + if ($('#text_r').val() == '') { + $('#text_r').val('0') + } + $('#text_r').val(parseInt($('#text_r').val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($('#text_r').val())) + canvas.renderAll() + recordState() + }) + $('#out_pic_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#out_pic_field_name').val() + updateList() + recordState() + }) + + $('#text_black').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].black = true + } else { + objs[idx - 1].black = false + } + recordState() + }) + // $("#out_pic_black").click(function () { + // let idx = getIndex(canvas.getActiveObject()); + // if ($(this).prop("checked")) { + // objs[idx-1].black = true; + // } else { + // objs[idx-1].black = false; + // } + // recordState(); + // }); + $('#out_text_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#out_text_field_name').val() + updateList() + recordState() + }) + $('#qrcode_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#qrcode_field_name').val() + updateList() + recordState() + }) + $('#qrcode_color').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0, foreground: $(this).val() }) + let idx = getIndex(item) + objs[idx - 1].color = $(this).val() + item.setSrc(qr, function (img) { + canvas.renderAll() + recordState() + }) + }) + $("#qrcode_val").bind('change', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $('#barcode_color').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].color = $(this).val() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + }) + $('#barcode_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#barcode_field_name').val() + updateList() + recordState() + }) + $('#text_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#text_color').val()) + canvas.renderAll() + recordState() + }) + $('#text_back_color').bind('input propertychange', function () { + canvas.getActiveObject().set('textBackgroundColor', $('#text_back_color').val()) + canvas.renderAll() + recordState() + }) + $('#out_text_back_color').bind('input propertychange', function () { + canvas.getActiveObject().set('textBackgroundColor', $('#out_text_back_color').val()) + + canvas.renderAll() + recordState() + }) + $("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll(); + updateList(); + recordState(); + }) + $("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + recordState(); + }); + $('#out_text_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#out_text_color').val()) + canvas.renderAll() + recordState() + }) + $("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $('#out_text_text').bind('input propertychange', function () { + let val = $("#out_text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll() + updateList() + recordState() + }) + $('#rect_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#rect_color').val()) + + canvas.renderAll() + recordState() + }) + $('#rect_stroke').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#rect_stroke').val()) + + canvas.renderAll() + recordState() + }) + $('#rect_stroke_width').bind('input propertychange', function () { + if ($('#rect_stroke_width').val() == '') { + $('#rect_stroke_width').val('0') + } else { + $('#rect_stroke_width').val(parseInt($('#rect_stroke_width').val())) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#rect_stroke_width').val())) + canvas.renderAll() + recordState() + }) + $('#rect_radius').bind('input propertychange', function () { + if ($('#rect_radius').val() == '') { + $('#rect_radius').val('0') + } else { + $('#rect_radius').val(parseInt($('#rect_radius').val())) + } + canvas.getActiveObject().set('rx', parseInt($('#rect_radius').val())) + canvas.getActiveObject().set('ry', parseInt($('#rect_radius').val())) + canvas.renderAll() + recordState() + }) + $('#circle_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#circle_color').val()) + + canvas.renderAll() + recordState() + }) + $('#circle_stroke').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#circle_stroke').val()) + + canvas.renderAll() + recordState() + }) + $('#circle_stroke_width').bind('input propertychange', function () { + if ($('#circle_stroke_width').val() == '') { + $('#circle_stroke_width').val('0') + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#circle_stroke_width').val())) + canvas.renderAll() + recordState() + }) + $('#counter_start').bind('input propertychange', function () { + canvas.getActiveObject().set('text', $('#counter_start').val()) + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].start = $(this).val() + canvas.renderAll() + recordState() + }) + $('#counter_add').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].add = $(this).val() + canvas.renderAll() + recordState() + }) + $('#counter_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#counter_color').val()) + canvas.renderAll() + recordState() + }) + $("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $('#line_color').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#line_color').val()) + canvas.renderAll() + recordState() + }) + $('#line_dist').bind('input propertychange', function () { + if ($('#line_dist').val() == '') { + $('#line_dist').val(0) + } + canvas.getActiveObject().set('strokeDashArray', [parseInt($('#line_dist').val()), parseInt($('#line_dist').val())]) + canvas.renderAll() + recordState() + }) + $('#text_text').bind('input propertychange', function () { + let val = $("#text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll() + updateList() + recordState() + }) + $('#barcode_val').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].val = $(this).val() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + }) + $("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val() + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + val = 0 + } else { + objs[idx - 1].show = false; + } + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val*dpi/72, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val*dpi/72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }) + $('#count_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() + }) + $('#text_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() + }) + + $('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + + $('#language_select').on('change', function () { + //语言选择 + langua_ge($(this).find('option:selected').val()) + s_lan = $(this).find('option:selected').val() + localStorage.setItem('lang', s_lan) + }) + $('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].font = $(this).find('option:selected').text() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + //recordState(); + }) + $('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() + }) + $('.obj_list').on('click', 'div', function () { + let idx = objs.length - 1 - $(this).index() + selectObj(idx) + }) + $('#exceed').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].exceed = true + } else { + objs[idx - 1].exceed = false + } + recordState() + }) + $('#autoWrap').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].autoWrap = true + } else { + objs[idx - 1].autoWrap = false + } + recordState() + }) + $('#fixSize').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].fixSize = true + } else { + objs[idx - 1].fixSize = false + } + recordState() + }) + $('#delete').click(function () { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + updateList() + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + } + // let actObjs = canvas.getActiveObjects(); + // let allObjs = canvas.getObjects(); + // let flag = 0; + // for( let item of actObjs){ + // if ( item != allObjs[0]){ + // let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + // objs.splice(i-1,1); + // canvas.remove(item); + // flag = 1; + // updateList(); + // } + // } + // selectObj(-1); + // if(flag == 1){ + // recordState(); + // } + }) + + //在关于我们获取版本号。 + // 注意:function 前面加了 async 关键字 + $("#about").click(async function () { + + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + console.error("获取版本号失败", e); + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
' + + '
' + + '
Soon Design
' + + // 【修改点】这里使用了刚才获取到的 version 变量 + '
' + language_str("vers") + ' ' + version + '
' + + '
' + language_str("copyright") + '
' + + '
' + }); + }) + $("#help").click(() => { + ipcRenderer.send('open-help-file'); + }); + function handleObjectSelected() { + let select_obj = canvas.getActiveObject() + if (!select_obj) { + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //comfirm + return + } + let i = getIndex(select_obj) + if (i != 0) { + //不是背景 + //console.log(objs); + --i + switch (objs[i].type) { + case 1: + //console.log("图片"); + $('#component_type').text(language_str('img')) //"图片" + selectObject(select_obj, i) + selectPic(select_obj, i) + break + case 2: + //console.log("预留图片"); + + $('#component_type').text(language_str('simg')) //"合成图片" + selectObject(select_obj, i) + selectOutPic(select_obj, i) + break + case 3: + $('#component_type').text(language_str('text')) //"文字" + //console.log("文字"); + selectObject(select_obj, i) + selectText(select_obj, i) + break + case 4: + //console.log("标签"); + + $('#component_type').text(language_str('stext')) //"合成文字" + selectObject(select_obj, i) + selectOutText(select_obj, i) + break + case 5: + //console.log("矩形"); + + $('#component_type').text(language_str('rect')) //"矩形" + selectObject(select_obj, i) + selectRect(select_obj, i) + break + case 6: + //console.log("圆形"); + + $('#component_type').text(language_str('circ')) //"圆形" + selectObject(select_obj, i) + selectCircle(select_obj, i) + break + case 7: + //console.log("直线"); + + $('#component_type').text(language_str('line')) //"直线" + selectObject(select_obj, i) + selectLine(select_obj, i) + break + case 8: + //console.log("二维码"); + + $('#component_type').text(language_str('qrc')) //"二维码" + selectObject(select_obj, i) + selectQrCode(select_obj, i) + break + case 9: + //console.log("条形码"); + + $('#component_type').text(language_str('barc')) //"条形码" + selectObject(select_obj, i) + selectBarCode(select_obj, i) + break + case 10: + //console.log("计数器"); + + $('#component_type').text(language_str('counter')) //"计数器" + selectObject(select_obj, i) + selectCounter(select_obj, i) + break + case 11: + + $("#component_type").text(language_str("ctext"));//"圆形文字" + //console.log("圆形文字"); + selectObject(select_obj, i); + selectCircleText(select_obj, i); + break; + } + } else { + //背景,收起对象属性 + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) + } + } + + canvas1.on('mouse:move', (e) => { + let width = canvas.getWidth() + let height = canvas.getHeight() + $('#posText').text( + 'X: ' + (e.pointer.x - (width / 2 - (width / 2 - background_image.left) * zoom)).toFixed(0) + ' Y: ' + (e.pointer.y - (height / 2 - (height / 2 - background_image.top) * zoom)).toFixed(0) + ) + }) + canvas1.on("object:moving", function (e) { + let obj = canvas1.getActiveObject() + let idx = getIndex(e.target, canvas1); + // 添加边界检查,避免访问 undefined + if (idx > 0 && idx <= objs1.length && objs1[idx - 1] && objs1[idx - 1].type && objs1[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { + x: obj.left, + y: obj.top + } + let distance = getDisdance(pointer.x, pointer.y, canvas1.width/2, canvas1.height/2) + let angleDeg = getDeg(pointer) + let flipped = pointer.y > canvas1.height/2 ? true : false + circle_text_diameter.setValue(distance*2-50); + obj.set("diameter", distance*2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); + }); + // 监听键盘事件 + + // 复制选中对象 + function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + console.log("未选中任何对象"); + layer.msg("未选中任何对象") + return; + } + + // 存储原始对象用于克隆 + clipboardData.objects = activeObjects; + console.log(activeObjects) + console.log(`已复制 ${activeObjects.length} 个对象`); + } + + // 粘贴对象 + function pasteFromClipboard() { + if (!clipboardData.objects || clipboardData.objects.length === 0) { + console.log("剪贴板为空"); + layer.msg("剪贴板为空") + return; + } + + // 克隆所有对象 + fabric.util.enlivenObjects( + clipboardData.objects.map(obj => obj.toObject()), + clonedObjects => { + // 设置新位置(添加偏移) + clonedObjects.forEach((clone, index) => { + let idx = getIndex(clipboardData.objects[index]) + console.log(clone) + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + // 计算整体偏移 + const groupOffset = clone.group; + if (groupOffset) { + clone.set({ + left: groupOffset.left + clipboard.offset, + top: groupOffset.top + clipboard.offset + }); + } + + // 添加到画布 + canvas.add(clone); + let tobj = {...objs[idx-1]} + if (tobj.name) tobj.name = tobj.name + objs.length + objs.push(tobj); + canvas.renderAll(); + updateList(); + recordState(); + }); + + // 选中粘贴的对象 + canvas.discardActiveObject(); + const sel = new fabric.ActiveSelection(clonedObjects, { + canvas: canvas + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + selectObj(objs.length - 1); + // 增加下次偏移 + clipboardData.offset += 10; + } + ); + } + canvas1.on('object:scaling', function (event) { + //if(objs[getIndex(event.target) - 1].type == 5 || objs[getIndex(event.target) - 1].type == 6){ + // //canvas.getActiveObject().set("width", canvas.getActiveObject().get("width") * canvas.getActiveObject().get("scaleX")); + // //canvas.getActiveObject().set("height", canvas.getActiveObject().get("height") * canvas.getActiveObject().get("scaleY")); + // //canvas.getActiveObject().set("scaleX", 1); + // //canvas.getActiveObject().set("scaleY", 1); + // //console.log(canvas.getActiveObject().get("scaleX")); + // //canvas1.discardActiveObject(); + // //canvas1.setActiveObject(event.target); + // //canvas1.renderAll(); + // //updateControls(); + //} + }) + canvas1.on('object:rotating', function (e) { + updateControls() + }) + canvas1.on('object:rotated', function (e) { + console.log('object:rotated') + recordState() + }) + canvas1.on('object:added', function (e) { + //console.log("object:added"); + //recordState(); + ////console.log(objs); + }) + canvas1.on('mouse:down', (e) => { + //单选 + let select_obj = canvas.getActiveObject() + console.log(select_obj) + let i = getIndex(select_obj) + if (i != 0) { + //不是背景 + //console.log(objs); + --i + switch (objs[i].type) { + case 1: + //console.log("图片"); + $('#component_type').text(language_str('img')) //"图片" + selectObject(select_obj, i) + selectPic(select_obj, i) + break + case 2: + //console.log("预留图片"); + + $('#component_type').text(language_str('simg')) //"合成图片" + selectObject(select_obj, i) + selectOutPic(select_obj, i) + break + case 3: + $('#component_type').text(language_str('text')) //"文字" + //console.log("文字"); + selectObject(select_obj, i) + selectText(select_obj, i) + break + case 4: + //console.log("标签"); + + $('#component_type').text(language_str('stext')) //"合成文字" + selectObject(select_obj, i) + selectOutText(select_obj, i) + break + case 5: + //console.log("矩形"); + + $('#component_type').text(language_str('rect')) //"矩形" + selectObject(select_obj, i) + selectRect(select_obj, i) + break + case 6: + //console.log("圆形"); + + $('#component_type').text(language_str('circ')) //"圆形" + selectObject(select_obj, i) + selectCircle(select_obj, i) + break + case 7: + //console.log("直线"); + + $('#component_type').text(language_str('line')) //"直线" + selectObject(select_obj, i) + selectLine(select_obj, i) + break + case 8: + //console.log("二维码"); + + $('#component_type').text(language_str('qrc')) //"二维码" + selectObject(select_obj, i) + selectQrCode(select_obj, i) + break + case 9: + //console.log("条形码"); + + $('#component_type').text(language_str('barc')) //"条形码" + selectObject(select_obj, i) + selectBarCode(select_obj, i) + break + case 10: + //console.log("计数器"); + + $('#component_type').text(language_str('counter')) //"计数器" + selectObject(select_obj, i) + selectCounter(select_obj, i) + break + case 11: + + $("#component_type").text(language_str("ctext"));//"圆形文字" + //console.log("圆形文字"); + selectObject(select_obj, i); + selectCircleText(select_obj, i); + break; + } + selectObj(i, true) //如果是点击画布中,则相应列表 + } else { + //背景,收起对象属性 + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + selectObj(-1) + $('#component_type').text(language_str('bg')) + } + }) + canvas1.on('text:editing:entered', function () { + _textEdit = true + }) + canvas1.on('text:editing:exited', function () { + _textEdit = false + }) + canvas1.on('object:modified', function (event) { + if (canvas1.getActiveObjects().length > 1) { + return + } + if (objs[getIndex(event.target) - 1].type == 3) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#text_size').val(event.target.fontSize) + canvas1.renderAll() + updateControls() + } else if (objs[getIndex(event.target) - 1].type == 4) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#out_text_size').val(event.target.fontSize) + canvas1.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 10) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#count_size').val(event.target.fontSize) + canvas1.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 5) { + canvas1.getActiveObject().set('width', canvas1.getActiveObject().get('width') * canvas1.getActiveObject().get('scaleX')) + canvas1.getActiveObject().set('height', canvas1.getActiveObject().get('height') * canvas1.getActiveObject().get('scaleY')) + canvas1.getActiveObject().set('scaleX', 1) + canvas1.getActiveObject().set('scaleY', 1) + updateControls() + } + recordState() + }) + canvas1.on('mouse:up', (e) => { + //HERE + switch (addState) { + case 1: + addPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addPic').css('background-color', '') + break + case 2: + addOutPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addOutPic').css('background-color', '') + break + case 3: + addText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addText').css('background-color', '') + break + case 4: + addOutText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addOutText').css('background-color', '') + break + case 5: + addRect(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addRect').css('background-color', '') + break + case 6: + addCircle(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addCircle').css('background-color', '') + break + case 7: + addLine(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addLine').css('background-color', '') + break + case 8: + addQrCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addQrCode').css('background-color', '') + break + case 9: + addBarCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addBarCode').css('background-color', '') + break + case 10: + addCounter(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + $('#addCounter').css('background-color', '') + break + case 11: + addCircleText(e.pointer); + addState = 0; + background_image.hoverCursor = "default"; + canvas1.renderAll(); + $("#addCircleText").css("background-color", ""); + break; + } + }) + canvas1.on('text:changed', (e) => { + $('#text_text').val(e.target.get('text')) + }) + canvas1.on('after:render', function () { + if (is_bgi_add) { + var image = document.getElementById('source_front') + ctx1.drawImage(image, (canvas.width - $('#source_front').width()) / 2, (canvas.height - $('#source_front').height()) / 2, $('#source_front').width(), $('#source_front').height()) + + /* + 实际905 * 534 + 1052 * + */ + } + }) + canvas2.on('text:editing:entered', function () { + _textEdit = true + }) + canvas2.on('text:editing:exited', function () { + _textEdit = false + }) + canvas2.on('after:render', function () { + if (is_bgi_add) { + var image = document.getElementById('source_back') + ctx2.drawImage(image, (canvas.width - $('#source_back').width()) / 2, (canvas1.height - $('#source_back').height()) / 2, $('#source_back').width(), $('#source_back').height()) + } + }) + canvas2.on('mouse:move', (e) => { + $('#posText').text('X: ' + (e.pointer.x - background_image.left).toFixed(0) + ' Y: ' + (e.pointer.y - background_image.top).toFixed(0)) + }) + canvas2.on("object:moving", function (e) { + let obj = canvas2.getActiveObject() + let idx = getIndex(e.target, canvas2); + // 添加边界检查,避免访问 undefined + if (idx > 0 && idx <= objs2.length && objs2[idx - 1] && objs2[idx - 1].type && objs2[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { + x: obj.left, + y: obj.top + } + let distance = getDisdance(pointer.x, pointer.y, canvas2.width/2, canvas2.height/2) + // 计算角度时需要使用 canvas2 + let centerX = canvas2.width / 2; + let centerY = canvas2.height / 2; + let mouseX = pointer.x; + let mouseY = pointer.y; + let dx = mouseX - centerX; + let dy = mouseY - centerY; + let angleRad = Math.atan2(dy, dx); + let angleDeg = angleRad * (180/Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90; + let flipped = pointer.y > canvas2.height/2 ? true : false + circle_text_diameter.setValue(distance*2-50); + obj.set("diameter", distance*2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); + }); + canvas2.on('object:scaling', function (e) { + updateControls() + }) + canvas2.on('object:rotating', function (e) { + updateControls() + }) + canvas2.on('object:rotated', function (e) { + console.log('object:rotated') + recordState() + }) + canvas2.on('object:added', function (e) { + //console.log("object:added"); + //recordState(); + ////console.log(objs); + }) + canvas2.on('mouse:down', (e) => { + //单选 + let select_obj = canvas.getActiveObject() + let i = getIndex(select_obj) + if (i != 0) { + //不是背景 + //console.log(objs); + --i + switch (objs[i].type) { + case 1: + //console.log("图片"); + $('#component_type').text(language_str('img')) //"图片" + selectObject(select_obj, i) + selectPic(select_obj, i) + break + case 2: + //console.log("预留图片"); + + $('#component_type').text(language_str('simg')) //"合成图片" + selectObject(select_obj, i) + selectOutPic(select_obj, i) + break + case 3: + $('#component_type').text(language_str('text')) //"文字" + //console.log("文字"); + selectObject(select_obj, i) + selectText(select_obj, i) + break + case 4: + //console.log("标签"); + + $('#component_type').text(language_str('stext')) //"合成文字" + selectObject(select_obj, i) + selectOutText(select_obj, i) + break + case 5: + //console.log("矩形"); + + $('#component_type').text(language_str('rect')) //"矩形" + selectObject(select_obj, i) + selectRect(select_obj, i) + break + case 6: + //console.log("圆形"); + + $('#component_type').text(language_str('circ')) //"圆形" + selectObject(select_obj, i) + selectCircle(select_obj, i) + break + case 7: + //console.log("直线"); + + $('#component_type').text(language_str('line')) //"直线" + selectObject(select_obj, i) + selectLine(select_obj, i) + break + case 8: + //console.log("二维码"); + + $('#component_type').text(language_str('qrc')) //"二维码" + selectObject(select_obj, i) + selectQrCode(select_obj, i) + break + case 9: + //console.log("条形码"); + + $('#component_type').text(language_str('barc')) //"条形码" + selectObject(select_obj, i) + selectBarCode(select_obj, i) + break + case 10: + //console.log("计数器"); + + $('#component_type').text(language_str('counter')) //"计数器" + selectObject(select_obj, i) + selectCounter(select_obj, i) + break + case 11: + + $("#component_type").text(language_str("ctext"));//"圆形文字" + //console.log("圆形文字"); + selectObject(select_obj, i); + selectCircleText(select_obj, i); + break; + } + selectObj(i, true) //如果是点击画布中,则相应列表 + } else { + //背景,收起对象属性 + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#counter_control').hide() + selectObj(-1) + $('#component_type').text(language_str('bg')) + } + }) + canvas2.on('object:modified', function (event) { + if (canvas.getActiveObjects().length > 1) { + return + } + if (objs[getIndex(event.target) - 1].type == 3) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#text_size').val(event.target.fontSize) + canvas2.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 4) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#out_text_size').val(event.target.fontSize) + canvas2.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 10) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#count_size').val(event.target.fontSize) + canvas2.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 5) { + canvas2.getActiveObject().set('width', canvas2.getActiveObject().get('width') * canvas2.getActiveObject().get('scaleX')) + canvas2.getActiveObject().set('height', canvas2.getActiveObject().get('height') * canvas2.getActiveObject().get('scaleY')) + canvas2.getActiveObject().set('scaleX', 1) + canvas2.getActiveObject().set('scaleY', 1) + updateControls() + } + console.log('object:modified') + recordState() + }) + canvas2.on('mouse:up', (e) => { + //HERE + switch (addState) { + case 1: + addPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addPic').css('background-color', '') + break + case 2: + addOutPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addOutPic').css('background-color', '') + break + case 3: + addText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addText').css('background-color', '') + break + case 4: + addOutText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addOutText').css('background-color', '') + break + case 5: + addRect(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addRect').css('background-color', '') + break + case 6: + addCircle(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addCircle').css('background-color', '') + break + case 7: + addLine(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addLine').css('background-color', '') + break + case 8: + addQrCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addQrCode').css('background-color', '') + break + case 9: + addBarCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addBarCode').css('background-color', '') + break + case 10: + addCounter(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addCounter').css('background-color', '') + break + case 11: + addCircleText(e.pointer); + addState = 0; + background_image.hoverCursor = "default"; + canvas2.renderAll(); + $("#addCircleText").css("background-color", ""); + break; + } + }) + var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, + max: 120 + , change: function (value) { + canvas.getActiveObject().set("fontSize", value); + canvas.renderAll(); + } + }); + var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 50, + max: 1100 + , change: function (value) { + canvas.getActiveObject().set("diameter", value); + canvas.renderAll(); + } + }); + var out_pic_op = slider.render({ + elem: '#out_pic_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + var pic_op = slider.render({ + elem: '#pic_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + var rect_op = slider.render({ + elem: '#rect_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + var circle_op = slider.render({ + elem: '#circle_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + + // 标尺绘制函数 + function initRulers() { + const dpi = 300; + const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) + const rulerHeight = 20; + const rulerWidth = 20; + + // 获取实际画布的像素尺寸(px) + // design2.html 的实际画布尺寸是 1012px x 648px + const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) + const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) + + // 获取显示区域的尺寸(包括 margin) + const canvasDiv = $('#canvas-div'); + const canvasWrapper = $('.canvas-wrapper'); + // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 + let canvasWidth = canvasWrapper.width(); + let canvasHeight = canvasWrapper.height(); + if (!canvasWidth || canvasWidth === 0) { + const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; + canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!canvasHeight || canvasHeight === 0) { + const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; + canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + + // 设置标尺尺寸(使用显示区域的尺寸) + $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); + $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); + $('.ruler-horizontal').css('width', canvasWidth + 'px'); + $('.ruler-vertical').css('height', canvasHeight + 'px'); + + // 绘制水平标尺 + function drawHorizontalRuler() { + const ctx = document.getElementById('ruler-h').getContext('2d'); + ctx.clearRect(0, 0, canvasWidth, rulerHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, canvasWidth, rulerHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvasWidth, rulerHeight); + ctx.beginPath(); + ctx.moveTo(0, rulerHeight - 1); + ctx.lineTo(canvasWidth, rulerHeight - 1); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) + // 显示区域宽度 = actualCanvasWidth * zoom (px) + // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const startX = 0; + const endX = canvasWidth; + + // 绘制主要刻度(每10mm) + for (let mm = 0; mm * pxPerMm <= endX; mm += 10) { + const x = startX + mm * pxPerMm; + if (x > endX) break; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + + // 数字 + ctx.fillText(mm + 'mm', x + 2, 2); + } + + // 绘制次要刻度(每1mm) + for (let mm = 0; mm * pxPerMm <= endX; mm += 1) { + const x = startX + mm * pxPerMm; + if (x > endX) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(x, rulerHeight - 8); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + } + } + + // 绘制垂直标尺 + function drawVerticalRuler() { + const ctx = document.getElementById('ruler-v').getContext('2d'); + ctx.clearRect(0, 0, rulerWidth, canvasHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, rulerWidth, canvasHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, rulerWidth, canvasHeight); + ctx.beginPath(); + ctx.moveTo(rulerWidth - 1, 0); + ctx.lineTo(rulerWidth - 1, canvasHeight); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const startY = 0; + const endY = canvasHeight; + + // 绘制主要刻度(每10mm) + for (let mm = 0; mm * pxPerMm <= endY; mm += 10) { + const y = startY + mm * pxPerMm; + if (y > endY) break; + + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + + // 数字(旋转90度) + ctx.save(); + ctx.translate(2, y + 8); + ctx.rotate(-Math.PI / 2); + ctx.fillText(mm + 'mm', 0, 0); + ctx.restore(); + } + + // 绘制次要刻度(每1mm) + for (let mm = 0; mm * pxPerMm <= endY; mm += 1) { + const y = startY + mm * pxPerMm; + if (y > endY) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(rulerWidth - 8, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + } + } + + // 初始绘制 + drawHorizontalRuler(); + drawVerticalRuler(); + + // 监听窗口大小变化 + $(window).on('resize', function() { + setTimeout(function() { + const canvasWrapper = $('.canvas-wrapper'); + let newWidth = canvasWrapper.width(); + let newHeight = canvasWrapper.height(); + if (!newWidth || newWidth === 0) { + const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; + newWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!newHeight || newHeight === 0) { + const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; + newHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + if (newWidth !== canvasWidth || newHeight !== canvasHeight) { + canvasWidth = newWidth; + canvasHeight = newHeight; + $('#ruler-h').attr('width', newWidth); + $('#ruler-v').attr('height', newHeight); + $('.ruler-horizontal').css('width', newWidth + 'px'); + $('.ruler-vertical').css('height', newHeight + 'px'); + drawHorizontalRuler(); + drawVerticalRuler(); + } + }, 100); + }); + + // 监听画布缩放变化 + canvas1.on('after:render', function() { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + canvas2.on('after:render', function() { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + } + }) diff --git a/frontend-electron/js/design2.js b/frontend-electron/js/design2.js new file mode 100644 index 0000000..65c85e4 --- /dev/null +++ b/frontend-electron/js/design2.js @@ -0,0 +1,530 @@ +// design2.js - 主入口(桌面端资源路径与 design2-back 一致:../assets/images/) +if (typeof require !== 'undefined') { + try { require('./common/fabric-ext.js'); } catch (e) {} +} +if (typeof window !== 'undefined') { + window.SOON_IMG = '../assets/images/'; + window.soonAsset = function (name) { + return window.SOON_IMG + String(name || '').replace(/^\/+/, ''); + }; +} + +// 过滤 Canvas2D willReadFrequently 警告(不影响功能,只是性能提示) +if (typeof console !== 'undefined' && console.warn) { + const originalWarn = console.warn; + console.warn = function(...args) { + const message = args.join(' '); + // 过滤掉 willReadFrequently 相关的警告 + if (message.includes('willReadFrequently') || message.includes('getImageData')) { + return; // 不输出这个警告 + } + originalWarn.apply(console, args); + }; +} + +// 平台桥由 design2.html 注入 js/platform/electron.js +var remote = typeof window !== 'undefined' ? window.remote : null; +var path = typeof window !== 'undefined' ? window.path : null; +var exePath = typeof window !== 'undefined' ? window.exePath : ''; +var ipcRenderer = typeof window !== 'undefined' ? window.ipcRenderer : null; +var dialog = typeof window !== 'undefined' ? window.dialog : null; +var clipboard = typeof window !== 'undefined' ? window.clipboard : null; +var jrQrcode, JsBarcode; +if (typeof require !== 'undefined') { + try { jrQrcode = require('jr-qrcode'); } catch (e) {} + try { JsBarcode = require('jsbarcode'); } catch (e) {} +} + +if (ipcRenderer) { + ipcRenderer.send('get-sys-fonts'); +} + +// 使用layui + layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { + let myDate = new Date(); + let s_lan = ""; + if (ipcRenderer) ipcRenderer.send('get-sys-language'); + + var $ = layui.$; + var layer = layui.layer; + var slider = layui.slider; + var form = layui.form; + var colorpicker = layui.colorpicker; + + // 确保所有必要的变量在全局作用域中可用(用于 .jsc 文件加载) + // 在 Electron 的渲染进程中,需要同时设置 window 和 global + if (typeof window !== 'undefined') { + window.$ = $; + window.jQuery = $; + window.ipcRenderer = ipcRenderer; + window.layer = layer; + window.slider = slider; + window.form = form; + window.colorpicker = colorpicker; + window.dialog = dialog; + window.remote = remote; + window.path = path; + window.jrQrcode = jrQrcode; + window.JsBarcode = JsBarcode; + window.clipboard = clipboard; + // design2 中没有定义 dpi,从 window.dpi 读取(在 design2.js 后面会设置) + } + // 在 Node.js 上下文中也设置(require 加载 .jsc 文件时使用) + if (typeof global !== 'undefined') { + global.$ = $; + global.jQuery = $; + global.ipcRenderer = ipcRenderer; + global.layer = layer; + global.slider = slider; + global.form = form; + global.colorpicker = colorpicker; + global.dialog = dialog; + global.remote = remote; + global.path = path; + global.jrQrcode = jrQrcode; + global.JsBarcode = JsBarcode; + global.clipboard = clipboard; + // design2 中没有定义 dpi,从 global.dpi 读取(在 design2.js 后面会设置) + } + var layer = layui.layer; + var slider = layui.slider; + var colorpicker = layui.colorpicker; + + // 历史记录文件路径(供 output.js 使用) + var fullPath = (path && exePath) ? path.join(exePath, 'data.json') : (typeof window !== 'undefined' ? window.fullPath : ''); + // 确保 fullPath 在全局作用域中可用(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.fullPath = fullPath; + window.exePath = exePath; + } + if (typeof global !== 'undefined') { + global.fullPath = fullPath; + global.exePath = exePath; + } + + // ========================================== + // 公共函数定义(需要在layui.use回调内部,因为依赖jQuery) + // 注意:所有函数都附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 + // ========================================== + + // DES加密函数 + window.desEncrypt = function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + var APIFMS; + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + } catch (err) { + } + return APIFMS; + }; + // 向后兼容 + function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + return window.desEncrypt(str, key); + } + + // 多语言切换 + window.langua_ge = function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)); + }); + $("[language='t']").each(function (i) { + $(this).attr("title", $(this).attr(lan)); + }); + }; + // 向后兼容 + function langua_ge(lan = 'zh') { + return window.langua_ge(lan); + } + + // 多语言字符串获取 + window.language_str = function language_str(str) { + let t = { + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, + "save": { zh: "保存", ozh: "保存", en: "Save" }, + "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, + "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, + "display": { zh: "预览", ozh: "預覽", en: "Display" }, + "output": { zh: "导出", ozh: "導出", en: "Export" }, + "bg": { zh: "背景", ozh: "背景", en: "Background" }, + "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, + "img": { zh: "圖片", ozh: "圖片", en: "Image" }, + "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, + "text": { zh: "文本", ozh: "文本", en: "Text" }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, + "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, + "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, + "line": { zh: "直線", ozh: "直線", en: "Line" }, + "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, + "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, + "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, + "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, + "vers": { zh: "版本", ozh: "版本", en: "Version" }, + "comf": { zh: "确认", ozh: "確認", en: "Select" }, + "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + }; + // 从全局作用域获取 s_lan(支持 .jsc 文件加载) + const currentLang = (typeof global !== 'undefined' && global.s_lan) ? global.s_lan : + (typeof window !== 'undefined' && window.s_lan) ? window.s_lan : s_lan; + return t[str] ? (t[str][currentLang] || t[str]['zh'] || str) : str; + }; + // 确保 language_str 在 global 作用域中也可用(用于 .jsc 文件加载) + if (typeof global !== 'undefined') { + global.language_str = window.language_str; + } + // 向后兼容 + function language_str(str) { + return window.language_str(str); + } + + // 获取URL参数 + window.GetFile = function GetFile() { + const params = new URLSearchParams(window.location.search); + return params; + }; + // 向后兼容 + function GetFile() { + return window.GetFile(); + } + + // 日期格式化 + window.getDate = function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); + }; + // 向后兼容 + function getDate() { + return window.getDate(); + } + + // 获取对象的绝对坐标 + window.getAbsoluteXY = function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")]; + }; + // 向后兼容 + function getAbsoluteXY(_obj) { + return window.getAbsoluteXY(_obj); + } + + // 计算两点之间的距离 + window.getDisdance = function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); + var dy = Math.abs(y2 - y1); + var distance = Math.sqrt(dx * dx + dy * dy); + return distance; + }; + // 向后兼容 + function getDisdance(x1, y1, x2, y2) { + return window.getDisdance(x1, y1, x2, y2); + } + + // 计算角度(相对于画布中心) + window.getDeg = function getDeg(pointer, canvas) { + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + var angleRad = Math.atan2(dy, dx); + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90; + return angleDeg; + }; + // 向后兼容 + function getDeg(pointer, canvas) { + return window.getDeg(pointer, canvas); + } + + // 检查字段名是否重复 + window.checkName = function checkName(name, objs1, objs2) { + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; + }; + // 向后兼容 + function checkName(name, objs1, objs2) { + return window.checkName(name, objs1, objs2); + } + + // 返回不重复的字段名 + window.resName = function resName(pre_name, objs1, objs2) { + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!window.checkName(pre_name + num, objs1, objs2)); + return pre_name + num; + }; + // 向后兼容 + function resName(pre_name, objs1, objs2) { + return window.resName(pre_name, objs1, objs2); + } + + // 获取对象在画布中的索引 + window.getIndex = function getIndex(target, canvas, _canvas = null) { + let temp_objs; + if (_canvas == null) { + temp_objs = canvas.getObjects(); + } else { + temp_objs = _canvas.getObjects(); + } + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; + }; + // 向后兼容 + function getIndex(target, canvas, _canvas = null) { + return window.getIndex(target, canvas, _canvas); + } + + // 获取对象类型 + window.getType = function getType(target, canvas, objs) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } + }; + // 向后兼容 + function getType(target, canvas, objs) { + return window.getType(target, canvas, objs); + } + + // 获取坐标的最小X值 + window.getCoordsMinX = function getCoordsMinX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x; + } + } + return x; + }; + // 向后兼容 + function getCoordsMinX(acoords) { + return window.getCoordsMinX(acoords); + } + + // 获取坐标的最大X值 + window.getCoordsMaxX = function getCoordsMaxX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x; + } + } + return x; + }; + // 向后兼容 + function getCoordsMaxX(acoords) { + return window.getCoordsMaxX(acoords); + } + + // 获取坐标的最小Y值 + window.getCoordsMinY = function getCoordsMinY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y; + } + } + return y; + }; + // 向后兼容 + function getCoordsMinY(acoords) { + return window.getCoordsMinY(acoords); + } + + // 获取坐标的最大Y值 + window.getCoordsMaxY = function getCoordsMaxY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y; + } + } + return y; + }; + // 向后兼容 + function getCoordsMaxY(acoords) { + return window.getCoordsMaxY(acoords); + } + + // ========================================== + // 全局变量定义(所有模块共享) + // 注意:所有变量都附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 + // ========================================== + window.dpi = 300; // design2使用300 DPI + // 确保 dpi 在 global 作用域中也可用(用于 .jsc 文件加载) + if (typeof global !== 'undefined') { + global.dpi = window.dpi; + } + window.addState = 0; // 0是不添加 + window.background_image = undefined; + window.background_image1 = undefined; + window.background_image2 = undefined; + window.bg_version = 1; + window.zoom = 0; + window.pre_add_image = undefined; // 预添加的图片对象(用于addPic) + + // 内部剪贴板 + window.clipboardData = { + data: null, + offset: 10 // 粘贴位置偏移量 + }; + + // 文件管理 + window.openAs = { + _name: "", + set name(val) { + if (val == "") { + $("title").html('Soon Design'); + } else { + $("title").html('Soon Design - ' + val); + } + this._name = val; + }, + get name() { + return this._name + }, + }; + + // 对象数组和历史记录 + window.objs1 = []; + window.objs2 = []; + window.step1 = { val: 0 }; + window.step2 = { val: 0 }; + window.step = window.step1; + window.objs = window.objs1; + window.recordJson1 = []; + window.recordJson2 = []; + window.recordJson = window.recordJson1; + window.recordObjs1 = []; + window.recordObjs2 = []; + window.recordObjs = window.recordObjs1; + window.pre_objs1 = []; + window.pre_objs2 = []; + window.next_objs1 = []; + window.next_objs2 = []; + window.pre_objs = window.pre_objs1; + window.next_objs = window.next_objs1; + window.pre_json1 = []; + window.pre_json2 = []; + window.next_json1 = []; + window.next_json2 = []; + window.pre_json = window.pre_json1; + window.next_json = window.next_json1; + + // Canvas对象(将在core.js中初始化) + window.canvas1 = undefined; + window.canvas2 = undefined; + window.canvas = undefined; // 当前活动的画布(canvas1 或 canvas2) + window.ctx1 = undefined; + window.ctx2 = undefined; + window.is_bgi_add = false; + + // 为了兼容性,同时创建局部变量引用(向后兼容) + const dpi = window.dpi; + let addState = window.addState; + let background_image = window.background_image; + let background_image1 = window.background_image1; + let background_image2 = window.background_image2; + let bg_version = window.bg_version; + var zoom = window.zoom; + let pre_add_image = window.pre_add_image; + let clipboardData = window.clipboardData; + var openAs = window.openAs; + let objs1 = window.objs1; + let objs2 = window.objs2; + let step1 = window.step1; + let step2 = window.step2; + let step = window.step; + let objs = window.objs; + let recordJson1 = window.recordJson1; + let recordJson2 = window.recordJson2; + let recordJson = window.recordJson; + let recordObjs1 = window.recordObjs1; + let recordObjs2 = window.recordObjs2; + let recordObjs = window.recordObjs; + let pre_objs1 = window.pre_objs1; + let pre_objs2 = window.pre_objs2; + let next_objs1 = window.next_objs1; + let next_objs2 = window.next_objs2; + let pre_objs = window.pre_objs; + let next_objs = window.next_objs; + let pre_json1 = window.pre_json1; + let pre_json2 = window.pre_json2; + let next_json1 = window.next_json1; + let next_json2 = window.next_json2; + let pre_json = window.pre_json; + let next_json = window.next_json; + var canvas1 = window.canvas1; + var canvas2 = window.canvas2; + var canvas = window.canvas; + var ctx1 = window.ctx1; + var ctx2 = window.ctx2; + let is_bgi_add = window.is_bgi_add; + + // 加载 design2 模块:网页端在回调内动态加载(保证 $ 已存在),桌面端用 require+eval + var fs = typeof window !== 'undefined' ? window.fs : null; + var appPath = (remote && remote.app && remote.app.getAppPath) ? remote.app.getAppPath() : (typeof __dirname !== 'undefined' ? __dirname : '.'); + + if (typeof require === 'undefined') { + function loadScript(src, next) { + var s = document.createElement('script'); + s.src = src; + s.onload = s.onerror = function () { if (typeof next === 'function') next(); }; + document.body.appendChild(s); + } + loadScript('../js/design2/output.js', function () { + loadScript('../js/design2/core.js', function () { + loadScript('../js/design2/ui.js', function () {}); + }); + }); + return; + } + + const loadModule = (moduleName) => { + if (!path || !fs) return; + const jsPath = path.join(appPath, 'js', 'design2', moduleName + '.js'); + if (fs.existsSync(jsPath)) { + try { + eval(fs.readFileSync(jsPath, 'utf8')); + } catch (e) { + alert('加载 ' + moduleName + ' 失败!\n\n错误: ' + e.message); + } + } else { + alert('文件不存在!\n\n请检查:' + jsPath); + } + }; + loadModule('output'); + loadModule('core'); + loadModule('ui'); +}); diff --git a/frontend-electron/js/design2/core.js b/frontend-electron/js/design2/core.js new file mode 100644 index 0000000..322f5ab --- /dev/null +++ b/frontend-electron/js/design2/core.js @@ -0,0 +1,2546 @@ +// design2核心功能模块 +// 注意:此代码在layui.use回调函数内部执行 +// 全局变量已在主入口文件中定义,这里只需要初始化 + +$('#canvas1').attr('height', document.body.clientHeight - 100) +$('#canvas2').attr('height', document.body.clientHeight - 100) +$('#object_attribute').css('height', '470px') +$('#obj_list').css('height', document.body.clientHeight - 520) +$('.obj_list').css('height', $('#obj_list').height() - 32) + +$('.main').show() +$('#canvas1').attr('width', $('#canvas-div').width()) +$('#canvas2').attr('width', $('#canvas-div').width()) + +// 初始化Canvas对象 +// 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently +try { + const canvas1El = document.getElementById('canvas1'); + const canvas2El = document.getElementById('canvas2'); + if (canvas1El) { + canvas1El.getContext('2d', { willReadFrequently: true }); + } + if (canvas2El) { + canvas2El.getContext('2d', { willReadFrequently: true }); + } +} catch (e) { + // 如果浏览器不支持 willReadFrequently,忽略错误 +} + +canvas1 = new fabric.Canvas('canvas1') +canvas2 = new fabric.Canvas('canvas2') +// 同步到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.canvas1 = canvas1; +window.canvas2 = canvas2; +// 撤销/重做过程中使用的标志位,防止并发操作和误记录历史 +window._isApplyingHistory = false; +fabric.Object.prototype.objectCaching = false +ctx1 = canvas1.getSelectionContext() +ctx2 = canvas2.getSelectionContext() +is_bgi_add = false +// 同步到 window 对象 +window.ctx1 = ctx1; +window.ctx2 = ctx2; +window.is_bgi_add = is_bgi_add; +canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) +//console.log(canvas1.width / 2, canvas1.height / 2); +//console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200); +canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) +zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200 + +// 初始化标尺 +function initRulers() { + // dpi已在主入口文件中定义为全局变量 + const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) + const rulerHeight = 20; + const rulerWidth = 20; + + // 获取实际画布的像素尺寸(px) + // design2.html 的实际画布尺寸是 1012px x 648px + const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) + const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) + + // 获取显示区域的尺寸(包括 margin) + const canvasDiv = $('#canvas-div'); + const canvasWrapper = $('.canvas-wrapper'); + // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 + let canvasWidth = canvasWrapper.width(); + let canvasHeight = canvasWrapper.height(); + if (!canvasWidth || canvasWidth === 0) { + const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; + canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!canvasHeight || canvasHeight === 0) { + const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; + canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + + // 设置标尺尺寸(使用显示区域的尺寸) + $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); + $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); + $('.ruler-horizontal').css('width', canvasWidth + 'px'); + $('.ruler-vertical').css('height', canvasHeight + 'px'); + + // 绘制水平标尺 + function drawHorizontalRuler() { + const ctx = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true }); + ctx.clearRect(0, 0, canvasWidth, rulerHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, canvasWidth, rulerHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvasWidth, rulerHeight); + ctx.beginPath(); + ctx.moveTo(0, rulerHeight - 1); + ctx.lineTo(canvasWidth, rulerHeight - 1); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) + // 显示区域宽度 = actualCanvasWidth * zoom (px) + // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const marginOffset = 20; // canvas 的 margin-left: 20px + // 标尺0点应该在background_image的左上角位置 + // background_image在画布坐标系中的left值,需要转换到标尺坐标系 + // 画布从标尺的marginOffset位置开始,所以:标尺0点 = marginOffset + background_image.left (如果存在) + let originX = marginOffset; + if (typeof background_image !== 'undefined' && background_image) { + originX = marginOffset + background_image.left; + } + const startX = 0; + const endX = canvasWidth; + + // 绘制主要刻度(每10mm) + // 从0点向左绘制负值刻度 + for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) { + const x = originX + mm * pxPerMm; + if (x < startX) break; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + + // 数字(负值) + ctx.fillText(mm + 'mm', x + 2, 2); + } + // 从0点向右绘制正值刻度 + for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 10) { + const x = originX + mm * pxPerMm; + if (x > endX) break; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + + // 数字 + ctx.fillText(mm + 'mm', x + 2, 2); + } + + // 绘制次要刻度(每1mm) + // 从0点向左绘制负值刻度 + for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) { + const x = originX + mm * pxPerMm; + if (x < startX) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(x, rulerHeight - 8); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + } + // 从0点向右绘制正值刻度 + for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 1) { + const x = originX + mm * pxPerMm; + if (x > endX) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(x, rulerHeight - 8); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + } + } + + // 绘制垂直标尺 + function drawVerticalRuler() { + const ctx = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true }); + ctx.clearRect(0, 0, rulerWidth, canvasHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, rulerWidth, canvasHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, rulerWidth, canvasHeight); + ctx.beginPath(); + ctx.moveTo(rulerWidth - 1, 0); + ctx.lineTo(rulerWidth - 1, canvasHeight); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const marginOffset = 20; // canvas 的 margin-top: 20px (但还要加上ruler的高度) + const rulerHeightOffset = 20; // ruler 的高度 + // 标尺0点应该在background_image的左上角位置 + // background_image在画布坐标系中的top值,需要转换到标尺坐标系 + // 画布从标尺的marginOffset + rulerHeightOffset位置开始,所以:标尺0点 = marginOffset + rulerHeightOffset + background_image.top (如果存在) + let originY = marginOffset + rulerHeightOffset; + if (typeof background_image !== 'undefined' && background_image) { + originY = marginOffset + rulerHeightOffset + background_image.top; + } + const startY = 0; + const endY = canvasHeight; + + // 绘制主要刻度(每10mm) + // 从0点向上绘制负值刻度 + for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) { + const y = originY + mm * pxPerMm; + if (y < startY) break; + + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + + // 数字(旋转90度,负值) + ctx.save(); + ctx.translate(2, y + 8); + ctx.rotate(-Math.PI / 2); + ctx.fillText(mm + 'mm', 0, 0); + ctx.restore(); + } + // 从0点向下绘制正值刻度 + for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 10) { + const y = originY + mm * pxPerMm; + if (y > endY) break; + + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + + // 数字(旋转90度) + ctx.save(); + ctx.translate(2, y + 8); + ctx.rotate(-Math.PI / 2); + ctx.fillText(mm + 'mm', 0, 0); + ctx.restore(); + } + + // 绘制次要刻度(每1mm) + // 从0点向上绘制负值刻度 + for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) { + const y = originY + mm * pxPerMm; + if (y < startY) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(rulerWidth - 8, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + } + // 从0点向下绘制正值刻度 + for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 1) { + const y = originY + mm * pxPerMm; + if (y > endY) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(rulerWidth - 8, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + } + } + + // 初始绘制 + drawHorizontalRuler(); + drawVerticalRuler(); + + // 监听窗口大小变化 + $(window).on('resize', function () { + setTimeout(function () { + const canvasWrapper = $('.canvas-wrapper'); + let newWidth = canvasWrapper.width(); + let newHeight = canvasWrapper.height(); + if (!newWidth || newWidth === 0) { + const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; + newWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!newHeight || newHeight === 0) { + const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; + newHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + if (newWidth !== canvasWidth || newHeight !== canvasHeight) { + canvasWidth = newWidth; + canvasHeight = newHeight; + $('#ruler-h').attr('width', newWidth); + $('#ruler-v').attr('height', newHeight); + $('.ruler-horizontal').css('width', newWidth + 'px'); + $('.ruler-vertical').css('height', newHeight + 'px'); + drawHorizontalRuler(); + drawVerticalRuler(); + } + }, 100); + }); + + // 监听画布缩放/渲染变化,仅用于重绘标尺 + // 辅助线交给 Fabric 自己渲染,否则会出现“重影 / 变两三根”的现象 + canvas1.on('after:render', function () { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + canvas2.on('after:render', function () { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + + // --- 辅助线功能 (参考 design1) --- + // canvasWrapper 已在上面声明,直接使用 + const canvasBox = $('#canvas-div'); // design2 使用 canvas-div 作为容器 + + // 确保容器定位 + if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); + + // 定义临时拖拽线 DOM + let $dragLine = null; + + // 通用拖拽开始函数 + const startDragGuide = (type, e) => { + e.preventDefault(); + e.stopPropagation(); + + // 防止重复调用:如果正在拖拽,直接返回 + if ($dragLine && $dragLine.length > 0) { + return; + } + + // 创建临时显示的线 + $dragLine = $('
'); + canvasWrapper.append($dragLine); + + if (type === 'horizontal') { + // 从上往下拖 (水平线) + $dragLine.css({ + left: 0, width: '100%', height: '1px', top: e.offsetY + }); + } else { + // 从左往右拖 (垂直线) + $dragLine.css({ + top: 0, height: '100%', width: '1px', left: e.offsetX + }); + } + + // 绑定 document 移动事件 + $(document).on('mousemove.ruler', function (moveEvent) { + let wrapperOffset = canvasWrapper.offset(); + if (type === 'horizontal') { + let relY = moveEvent.pageY - wrapperOffset.top; + $dragLine.css('top', relY + 'px'); + } else { + let relX = moveEvent.pageX - wrapperOffset.left; + $dragLine.css('left', relX + 'px'); + } + }); + + // 绑定松开事件 (创建真实的 Fabric 线) + $(document).on('mouseup.ruler', function (upEvent) { + $(document).off('mousemove.ruler mouseup.ruler'); + + if (!$dragLine) return; + + // 获取最终位置 + let wrapperOffset = canvasWrapper.offset(); + let finalX = upEvent.pageX - wrapperOffset.left; + let finalY = upEvent.pageY - wrapperOffset.top; + + // 移除临时线 + $dragLine.remove(); + $dragLine = null; + + // 只有拖入画布区域才创建 + if (finalX > rulerWidth && finalY > rulerHeight) { + createFabricGuide(type, finalX, finalY); + } + }); + }; + + // 绑定到标尺 DOM + $('#ruler-h').off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); + $('#ruler-v').off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); + + // --- 在画布中创建辅助线 --- + function createFabricGuide(type, domX, domY) { + // domX 和 domY 是相对于 canvas-wrapper 的坐标 + // 需要转换为相对于 canvas 的坐标,然后再转换为 Fabric 画布坐标 + + // 获取 canvas 相对于 canvas-wrapper 的位置 + let canvas1El = $('#canvas1'); + let canvasPosition = canvas1El.position(); + let canvasMarginLeft = parseFloat(canvas1El.css('margin-left')) || 0; + let canvasMarginTop = parseFloat(canvas1El.css('margin-top')) || 0; + + // 计算相对于 canvas 的坐标(减去 canvas 在 wrapper 中的位置和 margin) + let relX = domX - canvasPosition.left - canvasMarginLeft; + let relY = domY - canvasPosition.top - canvasMarginTop; + + // 转为 Fabric 坐标 (除以 zoom) + let canvasX = relX / zoom; + let canvasY = relY / zoom; + + // 使用与画布尺寸相关的长度,避免“整块区域都像辅助线” + let lineExtent = Math.max(canvas1.width, canvas1.height) * 2; + + // 构造线对象 + let linePoints = []; + if (type === 'horizontal') { + linePoints = [-lineExtent, canvasY, lineExtent, canvasY]; + } else { + linePoints = [canvasX, -lineExtent, canvasX, lineExtent]; + } + + let guideLine = new fabric.Line(linePoints, { + stroke: '#0066FF', + strokeWidth: 1 / zoom, + opacity: 0.9, + shadow: { + color: '#0066FF', + blur: 6, + offsetX: 0, + offsetY: 0, + affectStroke: true + }, + selectable: true, + evented: true, + hasControls: false, + hasBorders: false, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', + // 只在靠近线条时触发命中,避免“大块区域都像辅助线” + // 同时适当放宽 tolerance,保证易于点中拖拽 + perPixelTargetFind: true, + targetFindTolerance: 6, + padding: 2, + isGuideLine: true, + ignoreSave: true + }); + + // 限制移动方向(只约束方向,不把整条线拖到极远,避免“整片区域是辅助线”的感觉) + guideLine.on('moving', function () { + if (type === 'horizontal') { + // 水平线:根据当前中心点的 Y,重算 x1/x2,使其横穿画布 + const cy = this.getCenterPoint().y; + this.set({ + x1: -lineExtent, + y1: cy, + x2: lineExtent, + y2: cy + }); + } else { + // 垂直线:根据当前中心点的 X,重算 y1/y2,使其竖穿画布 + const cx = this.getCenterPoint().x; + this.set({ + x1: cx, + y1: -lineExtent, + x2: cx, + y2: lineExtent + }); + } + }); + + // 确定应该添加到哪个 canvas(使用当前活动的 canvas) + let targetCanvas = canvas; + if (!targetCanvas || (targetCanvas !== canvas1 && targetCanvas !== canvas2)) { + // 如果没有活动的 canvas,默认使用 canvas1 + targetCanvas = canvas1; + } + + targetCanvas.add(guideLine); + targetCanvas.setActiveObject(guideLine); + targetCanvas.renderAll(); + } + +} +initRulers(); +$('#source_front').width(zoom * $('#source_front').width()) +$('#source_back').width(zoom * $('#source_back').width()) +$('.canvas-container:eq(1)').hide() +canvas = canvas1 +window.canvas = canvas +fabric.Object.prototype.set({ + borderColor: '#9013FE', + cornerColor: '#9013FE', //激活状态角落图标的填充颜色 + cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 1, + cornerSize: 6, + cornerStyle: 'circle', //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0 +}) +canvas1.preserveObjectStacking = true +canvas2.preserveObjectStacking = true + +// ========================================== +// 对象选择和属性更新函数 +// ========================================== +// 将 updateControls 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.updateControls = function updateControls() { + // 使用 window.canvas 和 window.background_image 作为后备,确保在 eval() 执行时也能访问 + const currentCanvas = window.canvas || canvas; + const currentBackgroundImage = window.background_image || background_image; + if (!currentCanvas || !currentCanvas.getActiveObject()) return; + $("#text_r").val(currentCanvas.getActiveObject().get("angle").toFixed(0)); + $("#text_x").val((currentCanvas.getActiveObject().get("left") - currentBackgroundImage.left).toFixed(0)); + $("#text_y").val((currentCanvas.getActiveObject().get("top") - currentBackgroundImage.top).toFixed(0)); + $("#text_h").val((currentCanvas.getActiveObject().get("scaleY") * currentCanvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((currentCanvas.getActiveObject().get("scaleX") * currentCanvas.getActiveObject().get("width")).toFixed(0)); + // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) + if (currentCanvas.getActiveObject().get("lockMovementX")) { + $("#lock_img").attr("src", soonAsset("lock20.png"))//已锁定 + } else { + $("#lock_img").attr("src", soonAsset("unlock20.png"))//未锁定 + } +}; +// 向后兼容 +function updateControls() { + return window.updateControls(); +} +function selectObject(target, index) { + updateControls(); + $("#base_control").show(); +} + +function selectPic(target, index) { + // 检查是否有选中对象,如果没有则直接返回 + if (!canvas || !canvas.getActiveObject()) return; + + $("#pic_control").show(); + $("#set_bg").show(); + let activeObj = canvas.getActiveObject(); + + // 检查对象是否被锁定: + // - 从画布点击时(_selectingFromList 为 false):不允许选中,被锁定的对象保持不可选状态 + // - 从对象列表点击时(_selectingFromList 为 true):允许选中,但对象在画布上仍然保持锁定 + if (!window._selectingFromList && isObjectLocked(activeObj)) { + canvas.discardActiveObject(); + canvas.renderAll(); + return; + } + + if (typeof pic_op !== 'undefined' && pic_op && typeof pic_op.setValue === 'function') { + pic_op.setValue(100 - (activeObj.get("opacity") * 100)); + } else { + $("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%"); + $("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%"); + } + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectOutPic(target, index) { + $("#out_pic_control").show(); + if (typeof out_pic_op !== 'undefined' && out_pic_op.setValue) { + out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#out_pic_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#out_pic_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#out_pic_field_name").val(objs[idx - 1].name); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectText(target, index) { + $("#text_control").show(); + $("#text_color").val(canvas.getActiveObject().get("fill")); + $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#text_size").val(val); + $("#text_text").val(canvas.getActiveObject().get("text")); + $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#text_underline").addClass("ui-radio-acitve") + } else { + $("#text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#text_bold").addClass("ui-radio-acitve") + } else { + $("#text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#text_italics").addClass("ui-radio-acitve") + } else { + $("#text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#text_linethrough").addClass("ui-radio-acitve") + } else { + $("#text_linethrough").removeClass("ui-radio-acitve") + } + $("#circle_text_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectCircleText(target, index) { + const activeObj = target || canvas.getActiveObject(); + if (!activeObj) return; + + let idx = getIndex(activeObj); + $("#circle_text_control").show(); + $("#circle_text_color").val(activeObj.get("fill")); + $("#circle_text_back_color").val(activeObj.get("textBackgroundColor") == "" ? '#ffffff' : activeObj.get("textBackgroundColor")); + + if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') { + if (typeof circle_text_size_updating !== 'undefined') { + circle_text_size_updating = true; + } + circle_text_size.setValue(activeObj.get("fontSize") - 10); + if (typeof circle_text_size_updating !== 'undefined') { + circle_text_size_updating = false; + } + } + + if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') { + circle_text_diameter.setValue(activeObj.get("diameter") - 50); + } + + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(activeObj.get("text")); + $("#circle_text_font_family").val(activeObj.get("fontFamily")); + + if (activeObj.get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (activeObj.get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (activeObj.get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (activeObj.get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectOutText(target, index) { + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $("#out_text_control").show(); + $("#out_text_color").val(canvas.getActiveObject().get("fill")); + $("#out_text_text").val(canvas.getActiveObject().get("text")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#out_text_size").val(val); + $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#out_text_field_name").val(objs[idx - 1].name); + $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); + $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); + $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); + if (canvas.getActiveObject().get("underline") == true) { + $("#out_text_underline").addClass("ui-radio-acitve") + } else { + $("#out_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#out_text_bold").addClass("ui-radio-acitve") + } else { + $("#out_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#out_text_italics").addClass("ui-radio-acitve") + } else { + $("#out_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#out_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#out_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectRect(target, index) { + $("#rect_control").show(); + $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + $("#rect_radius").val(canvas.getActiveObject().get("rx")); + if (typeof rect_op !== 'undefined' && rect_op.setValue) { + rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#rect_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#rect_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectCircle(target, index) { + $("#circle_control").show(); + $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + if (typeof circle_op !== 'undefined' && circle_op.setValue) { + circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + } else { + $("#circle_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + $("#circle_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%"); + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectLine(target, index) { + $("#line_control").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#line_color").val(canvas.getActiveObject().get("stroke")); + $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function selectQrCode(target, index) { + $("#qrcode_Controll").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#qrcode_field_name").val(objs[idx - 1].name); + $("#qrcode_color").val(objs[idx - 1].color); + $("#qrcode_val").val(objs[idx - 1].val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#barcode_controll").hide(); +} + +function selectBarCode(target, index) { + $("#barcode_controll").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#barcode_field_name").val(objs[idx - 1].name); + $("#barcode_font_family").val(objs[idx - 1].font); + let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#barcode_size").val(val); + $("#barcode_val").val(objs[idx - 1].val); + $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); +} + +function selectCounter(target, index) { + $("#counter_control").show(); + let idx = getIndex(canvas.getActiveObject(), canvas); + $("#counter_start").val(objs[idx - 1].start); + $("#counter_add").val(objs[idx - 1].add); + $("#counter_color").val(canvas.getActiveObject().get("fill")); + $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#count_size").val(val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); +} + +function handleObjectSelected(e) { + // 确定当前活动的 canvas(从事件参数或全局变量) + let currentCanvas = (e && e.target && (e.target.canvas === canvas1 || e.target.canvas === canvas2)) + ? (e.target.canvas === canvas1 ? canvas1 : canvas2) + : canvas; + + // 如果事件来自 canvas,更新全局 canvas 变量 + if (currentCanvas === canvas1 || currentCanvas === canvas2) { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + window.canvas = canvas; + window.objs = objs; + } + + let select_obj = canvas.getActiveObject(); + + // 1. 未选中任何对象 + if (!select_obj) { + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); // 传 true,只更新UI + return; + } + + // 2. 如果是辅助线,直接返回,不处理(在获取索引之前检查) + if (select_obj.isGuideLine) { + return; + } + + // 3. 获取索引 + let i = getIndex(select_obj, canvas); + + // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 + if (!window._selectingFromList && isObjectLocked(select_obj)) { + canvas.discardActiveObject(); + canvas.renderAll(); + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); + return; + } + + if (i != 0) { + // --- 选中了普通对象 --- + --i; // 转换为 objs 数组下标 + + // 安全检查:确保 objs[i] 存在且有 type 属性 + if (objs[i] && typeof objs[i].type !== 'undefined') { + switch (objs[i].type) { + case 1: // 图片 + $("#component_type").text(language_str("img")); + selectObject(select_obj, i); selectPic(select_obj, i); break; + case 2: // 合成图片 + $("#component_type").text(language_str("simg")); + selectObject(select_obj, i); selectOutPic(select_obj, i); break; + case 3: // 文本 + $("#component_type").text(language_str("text")); + selectObject(select_obj, i); selectText(select_obj, i); break; + case 4: // 合成文本 + $("#component_type").text(language_str("stext")); + selectObject(select_obj, i); selectOutText(select_obj, i); break; + case 5: // 矩形 + $("#component_type").text(language_str("rect")); + selectObject(select_obj, i); selectRect(select_obj, i); break; + case 6: // 圆形 + $("#component_type").text(language_str("circ")); + selectObject(select_obj, i); selectCircle(select_obj, i); break; + case 7: // 直线 + $("#component_type").text(language_str("line")); + selectObject(select_obj, i); selectLine(select_obj, i); break; + case 8: // 二维码 + $("#component_type").text(language_str("qrc")); + selectObject(select_obj, i); selectQrCode(select_obj, i); break; + case 9: // 条形码 + $("#component_type").text(language_str("barc")); + selectObject(select_obj, i); selectBarCode(select_obj, i); break; + case 10: // 计数器 + $("#component_type").text(language_str("counter")); + selectObject(select_obj, i); selectCounter(select_obj, i); break; + case 11: // 圆形文本 + $("#component_type").text(language_str("ctext")); + selectObject(select_obj, i); selectCircleText(select_obj, i); break; + } + } + + // 【关键】这里传入 true! + // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 + selectObj(i, true); + + } else { + // --- 选中了背景 --- + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + selectObj(-1, true); + $("#component_type").text(language_str("bg")); + } +} + +function handleObjectMoving(e) { + let obj = e.target; + // 跳过辅助线 + if (obj.isGuideLine) { + updateControls(); + return; + } + let idx = getIndex(obj, canvas); + // 圆形文本特殊处理 + // 安全检查:确保 objs[idx - 1] 存在且有 type 属性 + if (idx > 0 && idx <= objs.length && objs[idx - 1] && typeof objs[idx - 1].type !== 'undefined' && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { x: obj.left, y: obj.top }; + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); + let angleDeg = getDeg(pointer); + let flipped = pointer.y > canvas.height / 2 ? true : false; + + if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter.setValue) { + circle_text_diameter.setValue(distance * 2 - 50); + } + obj.set("diameter", distance * 2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); +} + +// 辅助函数:检查对象是否被锁定 +// 只有当 lockMovementX 为 true 时才认为对象被锁定 +// selectable 为 false 可能是其他原因(如背景图),所以只检查 lockMovementX +function isObjectLocked(obj) { + if (!obj) return false; + // 主要检查 lockMovementX,这是锁定的主要标志 + return obj.get('lockMovementX') === true; +} +// 暴露到全局作用域,确保其他模块可以访问 +window.isObjectLocked = isObjectLocked; + +// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存) +// 包含所有需要保存的属性,特别是自定义对象的属性(如 CurvedText 的 diameter, flipped) +const TO_JSON_PROPERTIES = [ + "selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", + "evented", "linethrough", "textBackgroundColor", "diameter", "flipped" +]; +// 暴露到全局作用域 +window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES; + +// 辅助函数:解锁所有对象(加载时使用) +function unlockAllObjects(canvas) { + if (!canvas) return; + const objects = canvas.getObjects(); + objects.forEach((obj, index) => { + // 背景对象(索引0)保持锁定状态 + if (index === 0) { + obj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + } else { + // 其他对象默认解锁 + obj.set({ + lockMovementX: false, + lockMovementY: false, + lockRotation: false, + lockScalingX: false, + lockScalingY: false, + lockSkewingX: false, + lockSkewingY: false, + selectable: true, + evented: true + }); + } + }); +} +// 暴露到全局作用域 +window.unlockAllObjects = unlockAllObjects; + +// 将 handleObjectSelected 附加到 window 对象,确保全局可访问 +window.handleObjectSelected = handleObjectSelected; + +// 防止重复初始化事件监听器 +var _canvasEventsInitialized = false; + +function initCanvasEvents() { + // 如果已经初始化过,直接返回 + if (_canvasEventsInitialized) { + return; + } + _canvasEventsInitialized = true; + + [canvas1, canvas2].forEach(currentCanvas => { + // 1. 鼠标移动 + currentCanvas.on("mouse:move", (e) => { + let offsetX = 0, offsetY = 0; + if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { + offsetX = background_image.left; + offsetY = background_image.top; + } + if (e.pointer) { + $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); + } + }); + + // 2. 鼠标移出 + currentCanvas.on("mouse:out", () => { + // 可以在这里隐藏标尺辅助线 + }); + + // 3. 鼠标松开 (添加对象) + currentCanvas.on("mouse:up", (e) => { + if (addState === 0) return; + const ptr = e.pointer; + switch (addState) { + case 1: addPic(ptr); break; + case 2: addOutPic(ptr); break; + case 3: addText(ptr); break; + case 4: addOutText(ptr); break; + case 5: addRect(ptr); break; + case 6: addCircle(ptr); break; + case 7: addLine(ptr); break; + case 8: addQrCode(ptr); break; + case 9: addBarCode(ptr); break; + case 10: addCounter(ptr); break; + case 11: addCircleText(ptr); break; + } + addState = 0; + if (typeof background_image !== 'undefined') background_image.hoverCursor = "default"; + currentCanvas.renderAll(); + $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); + }); + + // 4. 选中处理 + currentCanvas.on('mouse:down', (e) => { + // 更新全局 canvas 变量 + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + // 阻止被锁定的对象被框选选中,同时彻底排除辅助线 + currentCanvas.on('before:selection:created', (e) => { + if (!e.targets || e.targets.length === 0) return; + + // 调试日志:原始选区 + console.log('[Guide] before:selection:created raw targets', e.targets.map(t => ({ + isGuideLine: !!t.isGuideLine, + type: t.type, + left: t.left, + top: t.top + }))); + + // 选区里彻底排除辅助线:不允许辅助线进入 ActiveSelection + // 只保留未锁定的普通对象 + e.targets = e.targets.filter(target => { + if (target.isGuideLine) return false; // 完全移除辅助线 + return !isObjectLocked(target); // 过滤掉被锁定的普通对象 + }); + + // 调试日志:过滤后的选区 + console.log('[Guide] before:selection:created filtered targets', e.targets.map(t => ({ + isGuideLine: !!t.isGuideLine, + type: t.type, + left: t.left, + top: t.top + }))); + }); + + // 添加 selection 事件以确保对象选中时正确显示属性面板 + // 注意:真正排除辅助线的逻辑已经在 before:selection:created 里完成, + // 这里不再对 ActiveSelection 做任何重建,避免 Fabric 内部递归导致栈溢出。 + currentCanvas.on('selection:created', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + currentCanvas.on('selection:updated', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + handleObjectSelected(e); + }); + currentCanvas.on('object:selected', (e) => { + canvas = currentCanvas; + objs = (currentCanvas === canvas1) ? objs1 : objs2; + + // 如果不是通过对象列表选中的,且对象被锁定,则取消选中 + // 注意:只有真正被锁定的对象才会被取消选中,未锁定的对象应该正常选中 + if (!window._selectingFromList && e.target && isObjectLocked(e.target)) { + currentCanvas.discardActiveObject(); + currentCanvas.renderAll(); + return; + } + + handleObjectSelected(e); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + }); + + // 5. 对象移动 + currentCanvas.on("object:moving", (e) => { + // 阻止背景图(索引0)被移动(如果已锁定) + let movingObj = e.target; + let idx = getIndex(movingObj, currentCanvas); + if (idx === 0) { + let bgObj = currentCanvas.getObjects()[0]; + if (bgObj && bgObj.get('lockMovementX') === true) { + e.e.preventDefault(); + e.e.stopPropagation(); + return false; + } + } + handleObjectMoving(e); + }); + + // 6. 变换事件 + currentCanvas.on("object:rotating", updateControls); + currentCanvas.on("object:scaling", updateControls); + currentCanvas.on("object:rotated", () => { recordState(); }); + currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); + currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); + currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); + + // 7. 对象修改后 + currentCanvas.on('object:modified', function (event) { + if (currentCanvas.getActiveObjects().length > 1) return; + // 跳过辅助线 + if (event.target.isGuideLine) { + return; + } + let idx = getIndex(event.target, currentCanvas); + if (idx <= 0) return; + let objData = objs[idx - 1]; + // 安全检查:确保 objData 存在且有 type 属性 + if (!objData || typeof objData.type === 'undefined') { + return; + } + let target = event.target; + + if ([3, 4, 10].includes(objData.type)) { + target.fontSize = (target.fontSize * target.scaleX).toFixed(0); + target.scaleX = 1; target.scaleY = 1; + target._clearCache(); + if (objData.type == 3) $("#text_size").val(target.fontSize); + if (objData.type == 4) $("#out_text_size").val(target.fontSize); + if (objData.type == 10) $("#count_size").val(target.fontSize); + currentCanvas.renderAll(); + updateControls(); + } else if (objData.type == 5) { + target.width *= target.scaleX; target.height *= target.scaleY; + target.scaleX = 1; target.scaleY = 1; + updateControls(); + } + recordState(); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + }); + }); +} + +// 将 updateList 附加到 window 对象,确保全局可访问 +window.updateList = function updateList() { + const currentObjs = window.objs || objs; + + let str = ""; + let i = 0; + + const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; + const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; + const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; + + for (let item of currentObjs) { + let displayIndex = i + 1; + + let iconName = "rect.svg"; + switch (item.type) { + case 1: iconName = "images.svg"; break; + case 2: iconName = "images_add.svg"; break; + case 3: iconName = "text.svg"; break; + case 4: iconName = "text2.svg"; break; + case 5: iconName = "rect.svg"; break; + case 6: iconName = "round.svg"; break; + case 7: iconName = "line.svg"; break; + case 8: iconName = "qrcode.svg"; break; + case 9: iconName = "barcode.svg"; break; + case 10: iconName = "counter.svg"; break; + case 11: iconName = "text3.svg"; break; + } + + let nameText = item.name ? item.name : + (item.type === 3 ? "TEXT_" + displayIndex : + (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); + + str = `
+
+ + ${nameText} +
+ ${displayIndex} +
` + str; + i++; + } + $(".obj_list").html(str); + + $(".obj_list > div").on("click", function () { + let imgId = $(this).find("img").attr("id"); + if (imgId) { + selectObj(parseInt(imgId.replace("obj", ""), 10)); + } + }); +} + +// 标记是否通过对象列表选中(用于区分鼠标点击和列表点击) +window._selectingFromList = false; + +// 将 selectObj 附加到 window 对象,确保全局可访问 +window.selectObj = function selectObj(objIndices, isFromCanvas = false) { + let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); + + // 清除所有高亮 + $('.obj_list > div').css("background-color", "transparent"); + + $('.obj_list > div').each(function () { + let $img = $(this).find("img"); + if ($img.length === 0) return; + + let idIndex = parseInt($img.attr("id").replace("obj", ""), 10); + + if (indicesArray.includes(idIndex)) { + $(this).css("background-color", "#6F7A84"); + + // 自动滚动到可视区域 + if (isFromCanvas && indicesArray[0] === idIndex) { + this.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + } + }); + + // 如果是点击列表,触发画布选中 + if (!isFromCanvas && indicesArray.length === 1) { + let target = canvas.getObjects()[indicesArray[0] + 1]; + if (target) { + // 检查对象是否被锁定 + let isLocked = isObjectLocked(target); + + // 设置标志位,表示这是通过对象列表选中的(无论是否锁定) + window._selectingFromList = true; + + // 如果被锁定,临时设置 selectable 和 evented 为 true,以便能够选中 + if (isLocked) { + target.set({ + selectable: true, + evented: true + }); + } + + canvas.setActiveObject(target); + canvas.requestRenderAll(); + + // 如果对象被锁定,选中后立即恢复锁定状态(但保持选中状态) + if (isLocked) { + setTimeout(() => { + target.set({ + selectable: false, + evented: false + }); + canvas.requestRenderAll(); + }, 0); + } + + // 清除标志位(延迟清除,确保事件处理完成) + setTimeout(() => { + window._selectingFromList = false; + }, 10); + + handleObjectSelected(); + } + } +} + +// 将 recordState 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.recordState = function recordState() { + // 撤销/重做过程中不记录历史,避免产生“中间状态”,导致需要点多次才能回到预期 + if (window._isApplyingHistory) { + return; + } + // 确定当前是正面还是背面,直接使用对应的数组 + const isFront = $('#front_side').hasClass('ui-button-active'); + const currentStep = isFront ? (window.step1 || step1) : (window.step2 || step2); + const currentRecordObjs = isFront ? (window.recordObjs1 || recordObjs1) : (window.recordObjs2 || recordObjs2); + const currentRecordJson = isFront ? (window.recordJson1 || recordJson1) : (window.recordJson2 || recordJson2); + const currentObjs = window.objs || objs; + const currentCanvas = window.canvas || canvas; + + ++currentStep.val; + for (let i = currentStep.val; i < currentRecordObjs.length; i++) { + currentRecordObjs.pop(); + currentRecordJson.pop(); + } + currentRecordObjs.push(JSON.stringify(currentObjs)) + // 保存所有锁定相关的属性,确保锁定状态可以正确恢复 + let j = currentCanvas.toJSON(TO_JSON_PROPERTIES); + j.objects[0].hoverCursor = "default"; + currentRecordJson.push(j); + + // 同步更新 window 变量和局部变量 + if (isFront) { + window.step1 = currentStep; + window.recordObjs1 = currentRecordObjs; + window.recordJson1 = currentRecordJson; + window.step = currentStep; + window.recordObjs = currentRecordObjs; + window.recordJson = currentRecordJson; + step1 = currentStep; + recordObjs1 = currentRecordObjs; + recordJson1 = currentRecordJson; + step = currentStep; + recordObjs = currentRecordObjs; + recordJson = currentRecordJson; + } else { + window.step2 = currentStep; + window.recordObjs2 = currentRecordObjs; + window.recordJson2 = currentRecordJson; + window.step = currentStep; + window.recordObjs = currentRecordObjs; + window.recordJson = currentRecordJson; + step2 = currentStep; + recordObjs2 = currentRecordObjs; + recordJson2 = currentRecordJson; + step = currentStep; + recordObjs = currentRecordObjs; + recordJson = currentRecordJson; + } +}; +// 向后兼容 +function recordState() { + return window.recordState(); +} + +// 将 getIndex 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问 +window.getIndex = function getIndex(target, _canvas = null) { + let temp_objs; + // 使用 window.canvas 作为默认值,确保在 eval() 执行时也能访问 + const currentCanvas = _canvas || window.canvas || canvas; + if (!currentCanvas) { + return 0; + } + temp_objs = currentCanvas.getObjects(); + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; +}; +// 向后兼容 +function getIndex(target, _canvas = null) { + return window.getIndex(target, _canvas); +} + +function getType(target) { + // 如果是辅助线,返回 null + if (target.isGuideLine) { + return null; + } + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + // 安全检查:确保 objs[i] 存在且有 type 属性 + if (!objs[i] || typeof objs[i].type === 'undefined') { + return null; + } + return objs[i].type; + } + i++; + } + return null; +} + +function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; +} + +function resName(pre_name) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num));//重复了就再加1 + return pre_name + num; +} + +function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; +} + +function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg +} + +function getCoordsMinX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x; +} + +function getCoordsMaxX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x; +} + +function getCoordsMinY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y; +} + +function getCoordsMaxY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y; +} + +function addBackground() { + // 类型改变 + fabric.Image.fromURL(soonAsset('bg_front_2.png'), function (image) { + image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 + image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + image.setSrc(image.toDataURL(), function (image) { + background_image1 = image + background_image = background_image1 + window.background_image1 = background_image1; + window.background_image = background_image; + canvas1.add(image) + canvas1.renderAll() + + recordObjs1.push(JSON.stringify(objs1)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + recordJson1.push(canvas1.toJSON(props)) + }) + // 类型改变 + fabric.Image.fromURL(soonAsset('bg_back_2.png'), function (image) { + image.left = ($('#canvas2').width() - image.width) / 2 + image.top = ($('#canvas2').height() - image.height) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image + window.background_image2 = background_image2; + canvas2.add(image) + canvas2.renderAll() + + recordObjs2.push(JSON.stringify(objs2)) + recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES)) + + let file = GetFile().get('file'); + if (!file && typeof sessionStorage !== 'undefined' && !window.ipcRenderer) { + try { + file = sessionStorage.getItem('soondesign_open_file'); + if (file) { + sessionStorage.removeItem('soondesign_open_file'); + sessionStorage.removeItem('soondesign_open_type'); + } + } catch (e) {} + } + if (file && file != 'empty') { + var p = window.openFile(file); + if (p && p.then) p.then(function() {}, function() {}); + } + }) + }) + is_bgi_add = true + }) +} + +function addPic(pointer) { + pre_add_image.left = pointer.x + pre_add_image.top = pointer.y + + canvas.add(pre_add_image) + + objs.push({ type: 1, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addOutPic(pointer) { + fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) { + let width = canvas.getWidth() + let height = canvas.getHeight() + image.left = pointer.x + image.top = pointer.y + image.opacity = 1 + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image) + objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) + canvas.renderAll() + updateList() + recordState() + }) + }) +} + +function addText(pointer) { + // 设置默认字号为30px(像素值),这样显示的字号约为 30 * 72 / 300 = 7.2pt + let text = new fabric.IText('TEXT', { + left: pointer.x, + top: pointer.y, + fill: '#000000', + textBackgroundColor: '', + fontStyle: 'normal', + fontWeight: 'normal', + fontSize: 30, + underline: false, + linethrough: false, + fontFamily: 'Arial' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + canvas.add(text) + objs.push({ type: 3, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', { + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance * 2, + fontSize: 30, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height / 2 ? true : false + }); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false, circleCenter: 0 }); + canvas.renderAll(); + updateList(); + recordState(); +} + +function addOutText(pointer) { + let text = new fabric.Text('MERGE TEXT', { + left: pointer.x, + top: pointer.y, + fill: '#000000', + fontStyle: 'normal', + fontWeight: 'normal', + textBackgroundColor: '', + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + + canvas.add(text) + objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) + canvas.renderAll() + updateList() + recordState() +} + +function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 3, + width: 100, + height: 100 + }) + canvas.add(rect) + objs.push({ type: 5, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 3, + radius: 100, + strokeUniform: true + }) + canvas.add(circle) + objs.push({ type: 6, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addLine(pointer) { + let line = new fabric.Line([50, 100, 200, 100], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 3, + strokeDashArray: [0, 0], + }) + + line.setControlVisible('tl', false) + line.setControlVisible('tr', false) + line.setControlVisible('br', false) + line.setControlVisible('bl', false) + line.setControlVisible('mt', false) + line.setControlVisible('mb', false) + canvas.add(line) + objs.push({ type: 7, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +function addQrCode(pointer) { + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x + image.top = pointer.y + canvas.add(image) + objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) + canvas.renderAll() + updateList() + recordState() + }) +} + +function addBarCode(pointer) { + JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x + image.top = pointer.y + + canvas.add(image) + objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) + canvas.renderAll() + updateList() + recordState() + }) +} + +function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fill: '#000000' + }) + canvas.add(text) + objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) + canvas.renderAll() + updateList() + recordState() +} + +var front_list = [] +function applyFontList(data) { + var newFontList = [] + ;(data || []).map(function(item) { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')) + } else { + newFontList.push(item) + } + }) + front_list = newFontList + for (var i = 0; i < front_list.length; i++) { + var item = front_list[i] + $('#text_font_family').append("') + $("#circle_text_font_family").append("") + $('#out_text_font_family').append("') + $('#barcode_font_family').append("') + $('#count_font_family').append("') + } +} +if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { + ipcRenderer.on('font-list', function(event, data) { applyFontList(data); }) +} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) { + window.platformBridge.getSystemFonts().then(applyFontList) +} +function applySysLan(data) { + let lang = localStorage.getItem('lang') + if (lang) { + s_lan = lang + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge(lang) + $('#language_select').val(lang) + return + } + switch (data) { + case 'zh-CN': + s_lan = 'zh' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh': + s_lan = 'zh' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh-TW': + s_lan = 'ozh' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('ozh') + $('#language_select').val('ozh') + localStorage.setItem('lang', 'ozh') + break + case 'en': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-AU': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-CA': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-GB': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-NZ': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-US': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-ZA': + s_lan = 'en' + // 同步到全局作用域(用于 .jsc 文件加载) + if (typeof window !== 'undefined') { + window.s_lan = s_lan; + } + if (typeof global !== 'undefined') { + global.s_lan = s_lan; + } + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + } +} +if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { + ipcRenderer.on('sys-lan', function(event, data) { applySysLan(data); }) +} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getLocale) { + applySysLan(window.platformBridge.getLocale()) +} +function onCloseConfirm() { + layer.confirm( + '' + language_str('whetherSave') + '', + { + btn: [language_str('save'), language_str('noSave'), language_str('cancel')], + btn3: function (index, layero) { } + }, + function (index, layero) { + if (typeof window.output === 'function') { + window.output(function() { + if (ipcRenderer) ipcRenderer.send('run-close') + else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose() + }) + } + }, + function (index) { + if (ipcRenderer) ipcRenderer.send('run-close') + else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose() + } + ) +} +if (typeof ipcRenderer !== 'undefined' && ipcRenderer) { + ipcRenderer.on('close', function(event, message) { onCloseConfirm(); }) +} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.onClose) { + window.platformBridge.onClose(onCloseConfirm) +} + +addBackground() //添加背景 白色卡片 + +initAligningGuidelines(canvas1) +initAligningGuidelines(canvas2) + +// 初始化 Canvas 事件监听 +initCanvasEvents(); + +// 初始化所有 Slider +var circle_text_size_updating = false; // 标志位,防止循环更新 +var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, + max: 120, + value: 50, + change: function (value) { + if (circle_text_size_updating) return; + if (canvas && canvas.getActiveObject && canvas.getActiveObject()) { + canvas.getActiveObject().set('fontSize', value); + canvas.renderAll(); + if (typeof updateControls === 'function') { + updateControls(); + } + if (typeof recordState === 'function') { + recordState(); + } + } + } +}); + +var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 50, + max: 1100, + value: 500, + change: function (value) { + if (canvas && canvas.getActiveObject && canvas.getActiveObject()) { + canvas.getActiveObject().set('diameter', value); + canvas.renderAll(); + if (typeof updateControls === 'function') { + updateControls(); + } + if (typeof recordState === 'function') { + recordState(); + } + } + } +}); + +var out_pic_op = slider.render({ + elem: '#out_pic_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +var pic_op = slider.render({ + elem: '#pic_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +var rect_op = slider.render({ + elem: '#rect_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +var circle_op = slider.render({ + elem: '#circle_op', + min: 0, + max: 100, + value: 0, + change: function (value) { + if (canvas.getActiveObject()) { + canvas.getActiveObject().set('opacity', 1 - value / 100); + canvas.renderAll(); + updateControls(); + recordState(); + } + } +}); + +//canvas.setZoom(0.5) +//recordObjs1.push(JSON.stringify(objs1)); +//recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); +//recordObjs2.push(JSON.stringify(objs2)); +//recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); +canvas.renderAll() //重新渲染画布 +var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100) + zoom = val / 100 + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } +}) +window.rotate = function (id) { + let angle = $(`#${id}`).attr('angle') - 0 + angle += 90 + $(`#${id}`).css('transform', `rotate(${angle}deg)`) + $(`#${id}`).attr('angle', angle) +} +// 注意:#display 事件绑定已在 ui.js 中定义(第2633行),此处已删除重复定义 +// 导出文件 +// 将 savePdf 附加到 window 对象,确保在 output.js 中也能访问 +window.savePdf = async function savePdf(bufferOrBlob) { + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); + if (!dialogApi) return; + var defaultPath = (typeof path !== 'undefined' && path && exePath) ? path.join(exePath, 'output.pdf') : 'output.pdf'; + var result = await dialogApi.showSaveDialog({ + title: '保存文件', + defaultPath: defaultPath, + filters: [{ name: 'PDF 文件', extensions: ['pdf'] }] + }); + var canceled = result.canceled; + var filePath = result.filePath || (result.fileHandle && result.fileHandle.name); + if (canceled || (!filePath && !result.fileHandle)) return; + var name = (typeof filePath === 'string' ? filePath.split(/[/\\]/).pop() : 'output.pdf') || 'output.pdf'; + if (!/\.pdf$/i.test(name)) name = name + '.pdf'; + if (window.platformBridge && window.platformBridge.writeFile) { + var blob = bufferOrBlob instanceof Blob ? bufferOrBlob : (bufferOrBlob && bufferOrBlob.buffer ? new Blob([bufferOrBlob]) : new Blob([bufferOrBlob])); + window.platformBridge.writeFile(result.fileHandle || filePath, blob).then(function() { + layer.msg(language_str("saveSucc") + name); + }); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs && filePath) { + var buf = bufferOrBlob && bufferOrBlob.buffer ? Buffer.from(bufferOrBlob) : (bufferOrBlob instanceof Uint8Array ? Buffer.from(bufferOrBlob) : bufferOrBlob); + fs.writeFile(filePath, buf, function(err) { + if (!err) layer.msg(language_str("saveSucc") + filePath); + }); + } +}; +// 向后兼容 +async function savePdf(buffer) { + return window.savePdf(buffer); +} + +// display_func 函数已移至 output.js,避免重复定义 +// function display_func(img1, img2, img3) { ... } + +// output 函数已移至 output.js,避免重复定义 +// function output(callback = null, _save = save) { ... } + +var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false +$('input').focus(function () { + _inputEdit = true +}) +$('input').blur(function () { + _inputEdit = false +}) +$('textarea').focus(function () { + _inputEdit = true +}) +$('textarea').blur(function () { + _inputEdit = false +}) +$('body').keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false + } + if (event.keyCode == 67) { + _keyc = false + } +}) +$('body').keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $('#new').click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $('#open').click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $('#save').click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + if (typeof window.output === 'function') { + window.output(null, saveAs); + } + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $('#previous').click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $('#next').click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.pasteFromClipboard(); + return; + } + // CTRL/CMD+SHIFT+DEL - 清空 + if (isModifierKey && event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#clean').click(); + return; + } + // CTRL/CMD+DEL - 删除 + if (isModifierKey && !event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#delete').click(); + return; + } + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 67) { + _keyc = true + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 37) { + //left + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + if (canvas.getActiveObject()) { + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 38) { + //up + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + if (canvas.getActiveObject()) { + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 39) { + //right + let v = 1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + if (canvas.getActiveObject()) { + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 40) { + //down + let v = +1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + if (canvas.getActiveObject()) { + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item, canvas) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + updateList() + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + handleObjectSelected() + } + } +}) + +// 复制选中对象 +window.copySelection = function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + layer.msg("未选中任何对象") + return; + } + + // 【核心修复】遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 + clipboardData.data = activeObjects.map(obj => { + let idx = getIndex(obj, canvas); // 获取对象在画布中的索引 + + // 获取业务属性 (objs数组中对应的那一项) + // 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1 + let appData = (idx > 0 && objs[idx - 1]) ? objs[idx - 1] : { type: 0 }; + + return { + // 1. 保存画面样式 (坐标、颜色、字号等,包含图片的 src) + fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data', 'src']), + // 2. 保存业务属性 (深拷贝,防止引用冲突) + appData: JSON.parse(JSON.stringify(appData)) + }; + }); + + clipboardData.offset = 10; // 重置偏移量 + layer.msg("已复制"); +} + +// 粘贴对象 +window.pasteFromClipboard = function pasteFromClipboard() { + // 检查是否有数据 + if (!clipboardData.data || clipboardData.data.length === 0) { + layer.msg("剪贴板为空") + return; + } + + // 防止重复调用:如果正在粘贴,直接返回 + if (window._isPasting) { + return; + } + window._isPasting = true; + + // 1. 提取 Fabric JSON 准备恢复画面 + const jsonToEnliven = clipboardData.data.map(item => item.fabricJson); + + // 2. 恢复对象 (异步过程) + fabric.util.enlivenObjects(jsonToEnliven, function (objects) { + canvas.discardActiveObject(); + + objects.forEach((clone, index) => { + // --- A. 处理画面位置 --- + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + + // 如果是组内对象,需要特殊处理坐标 + if (clone.group) { + clone.set({ + left: clone.group.left + clipboardData.offset, + top: clone.group.top + clipboardData.offset + }); + } + + // --- B. 处理业务数据 --- + // 取出复制时保存的 appData + let savedAppData = clipboardData.data[index].appData; + let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝 + + // 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy) + if (newAppData.name) { + newAppData.name = newAppData.name + "_copy"; + } + + // --- C. 添加到画布和数据数组 --- + canvas.add(clone); + objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组 + }); + + // 3. 选中新粘贴出来的对象 + let sel = new fabric.ActiveSelection(objects, { canvas: canvas }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + + // 4. 刷新列表和历史记录 + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState(); + + // 5. 高亮左侧列表最后一项 (即新粘贴的项) + if (typeof window.selectObj === 'function') { + if (objs.length > 0) { + window.selectObj(objs.length - 1); + } + } + + // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) + clipboardData.offset += 10; + + // 清除粘贴标志 + window._isPasting = false; + }); +} + +// 将 onStateRestored 附加到 window 对象,确保全局可访问 +// 此函数在撤回/重做后调用,用于恢复画布状态 +window.onStateRestored = function onStateRestored() { + // 1. 重新获取背景图引用 (因为旧对象被销毁了) + // 注意:canvas 是全局变量,指向当前激活的画板 + if (canvas && canvas.getObjects().length > 0) { + background_image = canvas.getObjects()[0]; + background_image.selectable = false; // 确保背景不可选 + window.background_image = background_image; + background_image.evented = false; + } + + // 2. 渲染画布 + canvas.requestRenderAll(); + + // 3. 取消选中状态 + if (typeof window.selectObj === 'function') { + window.selectObj(-1); + } + + // 4. 隐藏所有属性面板 + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + + // 5. 刷新左侧列表 + if (typeof window.updateList === 'function') { + window.updateList(); + } +} diff --git a/lib/design2/output.js b/frontend-electron/js/design2/output.js similarity index 97% rename from lib/design2/output.js rename to frontend-electron/js/design2/output.js index 76bcd09..45be2fc 100644 --- a/lib/design2/output.js +++ b/frontend-electron/js/design2/output.js @@ -304,26 +304,26 @@ window.display_func = function display_func(img1, img2, img3) { if ((objs1.length != 0) & (objs2.length == 0)) { //只有正面 let img = new Image() - img.src = printPath3 + img.src = url3 pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) btns.push('打印正面') } else if ((objs1.length == 0) & (objs2.length != 0)) { //只有背面 let img = new Image() - img.src = printPath4 + img.src = url4 pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) btns.push('打印背面') } else { let img = new Image() - img.src = printPath3 + img.src = url3 pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) pdf.addPage() let img2 = new Image() - img2.src = printPath4 + img2.src = url4 pdf.addImage(img2, 'JPEG', 0, 0, 1012, 648) btns.push('打印正面') btns.push('打印背面') @@ -341,15 +341,15 @@ window.display_func = function display_func(img1, img2, img3) {
- +
- +
- +
- +
@@ -361,15 +361,14 @@ window.display_func = function display_func(img1, img2, img3) { } }, btn2: function () { - if (window.platformBridge && window.platformBridge.printPdf && (printPath3.indexOf('data:') === 0 || printPath4.indexOf('data:') === 0)) { - if (btns[1] === '打印正面') window.platformBridge.printPdf(pdfBlob); - else if (btns[1] === '打印背面') window.platformBridge.printPdf(pdfBlob); + if (window.platformBridge && window.platformBridge.printPdf) { + window.platformBridge.printPdf(pdfBlob); return; } if (btns[1] === '打印正面') { - printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' }); + printJS({ printable: url3, type: 'image', style: 'img { width: 100%; height: auto; }' }); } else if (btns[1] === '打印背面') { - printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }); + printJS({ printable: url4, type: 'image', style: 'img { width: 100%; height: auto; }' }); } }, btn3: function () { @@ -377,7 +376,7 @@ window.display_func = function display_func(img1, img2, img3) { window.platformBridge.printPdf(pdfBlob); return; } - printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }); + printJS({ printable: url4, type: 'image', style: 'img { width: 100%; height: auto; }' }); }, end: function () { // 预览窗口关闭后,恢复所有对象的 selectable 和 evented 状态 @@ -406,11 +405,11 @@ window.display_func = function display_func(img1, img2, img3) { // 将 output 附加到 window 对象,确保全局可访问 window.output = function output(callback = null, _save = save) { // 类型改变 - fabric.Image.fromURL('./public/images/op_2.png', function (i1) { + fabric.Image.fromURL(soonAsset('op_2.png'), function (i1) { i1.left = background_image.left i1.top = background_image.top let img1 = i1 - fabric.Image.fromURL('./public/images/op_2.png', function (i2) { + fabric.Image.fromURL(soonAsset('op_2.png'), function (i2) { i2.left = background_image.left i2.top = background_image.top let img2 = i2 @@ -1044,6 +1043,16 @@ function cleanupSrcFields(jsonObjects, objsArray) { return window.cleanupSrcFields(jsonObjects, objsArray); } +function isWebPortal() { + return !!(window.platformBridge && !window.ipcRenderer); +} +function resolveDiskPath(p) { + if (!p) return p; + var prefix = 'soondesign_session:'; + if (!isWebPortal() && p.indexOf(prefix) === 0) return p.substring(prefix.length); + return p; +} + function saveAs(op1, callback) { // 临时移除辅助线,保存后再恢复 let guideLines1 = [] @@ -1089,9 +1098,9 @@ function saveAs(op1, callback) { var content = JSON.stringify(con_o); if (window.platformBridge && window.platformBridge.writeFile) { window.platformBridge.writeFile(result.fileHandle || fp, content).then(function() { - openAs.name = 'soondesign_session:' + fp; + openAs.name = isWebPortal() ? ('soondesign_session:' + fp) : fp; webSessionStore(content, fp); - layer.msg(language_str('saveSucc') + fp); + layer.msg(language_str('saveSucc') + (isWebPortal() ? fp : openAs.name)); saveHistory(); if (callback && typeof callback === 'function') callback(); }); @@ -1145,7 +1154,7 @@ function save(op1, callback) { con_o.soonType = 2 var content = JSON.stringify(con_o) if (openAs.name != '') { - if (openAs.name.indexOf('soondesign_session:') === 0) { + if (isWebPortal() && openAs.name.indexOf('soondesign_session:') === 0) { webSessionStore(content, openAs.name); // 服务器环境:同时保存到服务器 var fileName = openAs.name.replace(/^soondesign_session:/, ''); @@ -1185,9 +1194,9 @@ function save(op1, callback) { if (ext !== '.soon') fp = (fp || 'design').replace(/\.soon$/i, '') + '.soon'; if (window.platformBridge && window.platformBridge.writeFile) { window.platformBridge.writeFile(result.fileHandle || fp, content).then(function() { - openAs.name = 'soondesign_session:' + fp; + openAs.name = isWebPortal() ? ('soondesign_session:' + fp) : fp; webSessionStore(content, fp); - layer.msg(language_str('saveSucc') + fp); + layer.msg(language_str('saveSucc') + (isWebPortal() ? fp : openAs.name)); saveHistory(); if (callback && typeof callback === 'function') callback(); }); @@ -1207,7 +1216,7 @@ function save(op1, callback) { // 网页端:写入 sessionStorage 与 localStorage,供首页历史读取(localStorage 跨标签/重开可用) function webSessionStore(content, fileName) { - if (!window.platformBridge || typeof sessionStorage === 'undefined') return; + if (!isWebPortal() || !window.platformBridge || typeof sessionStorage === 'undefined') return; var name = fileName || openAs.name || 'design.soon'; var key = (name && name.indexOf('soondesign_session:') === 0) ? name : ('soondesign_session:' + name); try { @@ -1219,9 +1228,9 @@ function webSessionStore(content, fileName) { // 将 saveHistory 附加到 window 对象,确保在 ui.js 中也能访问 window.saveHistory = function saveHistory() { function doWrite(j) { - var currentPath = openAs.name; + var currentPath = resolveDiskPath(openAs.name); var pathForHistory = currentPath; - if (window.platformBridge && (currentPath.indexOf('soondesign_session:') !== 0)) { + if (isWebPortal() && currentPath.indexOf('soondesign_session:') !== 0) { pathForHistory = 'soondesign_session:' + (currentPath || 'design.soon'); } var list = Array.isArray(j.history) ? j.history : []; @@ -1230,20 +1239,19 @@ window.saveHistory = function saveHistory() { if (newList.length > 20) newList = newList.slice(0, 20); j.history = newList; if (window.platformBridge && window.platformBridge.writeHistory) { - window.platformBridge.writeHistory(j); - return; + return window.platformBridge.writeHistory(j); } var fs = typeof require !== 'undefined' ? require('fs') : null; if (fs && typeof fullPath !== 'undefined' && fullPath) { try { fs.writeFileSync(fullPath, JSON.stringify(j), 'utf8'); } catch (e) {} } + return Promise.resolve(); } if (window.platformBridge && window.platformBridge.readHistory) { - window.platformBridge.readHistory().then(function(j) { + return window.platformBridge.readHistory().then(function(j) { if (!j || !Array.isArray(j.history)) j = { history: [] }; - doWrite(j); - }).catch(function() { doWrite({ history: [] }); }); - return; + return doWrite(j); + }).catch(function() { return doWrite({ history: [] }); }); } var fs = typeof require !== 'undefined' ? require('fs') : null; var j = { history: [] }; @@ -1254,7 +1262,7 @@ window.saveHistory = function saveHistory() { } catch (e) { j = { history: [] }; } if (!Array.isArray(j.history)) j.history = []; } - doWrite(j); + return doWrite(j); }; // 向后兼容 function saveHistory() { @@ -1281,10 +1289,10 @@ window.openFile = function open(file, jAlready) { } // 如果背景图片没有src,从路径恢复(因为保存时删除了src以减小文件大小) if (j.f.objects[0] && !j.f.objects[0].src) { - j.f.objects[0].src = './public/images/bg_front_2.png' + j.f.objects[0].src = soonAsset('bg_front_2.png') } if (j.b.objects[0] && !j.b.objects[0].src) { - j.b.objects[0].src = './public/images/bg_back_2.png' + j.b.objects[0].src = soonAsset('bg_back_2.png') } // 为二维码和条形码对象创建临时的 src(Fabric.js 需要 src 来创建 Image 对象) @@ -1299,7 +1307,7 @@ window.openFile = function open(file, jAlready) { j.f.objects[idx + 1].src = placeholder } else if (objMeta.type === 2 && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) { // 合成图片类型,恢复默认图片 - j.f.objects[idx + 1].src = './public/images/outpic_extra.png' + j.f.objects[idx + 1].src = soonAsset('outpic_extra.png') } }) @@ -1309,7 +1317,7 @@ window.openFile = function open(file, jAlready) { j.b.objects[idx + 1].src = placeholder } else if (objMeta.type === 2 && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) { // 合成图片类型,恢复默认图片 - j.b.objects[idx + 1].src = './public/images/outpic_extra.png' + j.b.objects[idx + 1].src = soonAsset('outpic_extra.png') } }) @@ -1455,6 +1463,7 @@ window.openFile = function open(file, jAlready) { doOpenWithJson(jAlready); return; } + file = resolveDiskPath(file); if (window.platformBridge && window.platformBridge.readJsonFile) { return window.platformBridge.readJsonFile(file).then(function(j) { if (j) { @@ -1465,7 +1474,8 @@ window.openFile = function open(file, jAlready) { } try { var fs = typeof require !== 'undefined' ? require('fs') : null; - if (!fs || file.indexOf('soondesign_session:') === 0) return; + if (!fs || (isWebPortal() && file.indexOf('soondesign_session:') === 0)) return; + if (!fs.existsSync(file)) return; var fsData = fs.readFileSync(file); var j = JSON.parse(fsData.toString()); doOpenWithJson(j); diff --git a/lib/design2/ui.js b/frontend-electron/js/design2/ui.js similarity index 96% rename from lib/design2/ui.js rename to frontend-electron/js/design2/ui.js index 10c5ff9..d28c48a 100644 --- a/lib/design2/ui.js +++ b/frontend-electron/js/design2/ui.js @@ -360,7 +360,7 @@ $('#version_change').click(function () { bg_version = 1 } // 类型改变 - $('#source_front').attr('src', './public/images/front_bg' + bg_version + '_2.png') + $('#source_front').attr('src', soonAsset('front_bg') + bg_version + '_2.png') canvas.renderAll() }) $('#front_side').click(function () { @@ -809,7 +809,7 @@ $('#set_bg').click(function () { selectable: false, evented: false }); - $("#lock_img").attr("src", "public/images/lock20.png") + $("#lock_img").attr("src", soonAsset("lock20.png")) canvas.sendToBack(canvas.getActiveObject()) canvas.bringForward(canvas.getActiveObject()) canvas.renderAll() @@ -1222,9 +1222,9 @@ $('#lock').off('click').on('click', function (e) { evented: !shouldLock }); if (shouldLock) { - $('#lock_img').attr('src', 'public/images/lock20.png') //已锁定 + $('#lock_img').attr('src', soonAsset('lock20.png')) //已锁定 } else { - $('#lock_img').attr('src', 'public/images/unlock20.png') //已解锁 + $('#lock_img').attr('src', soonAsset('unlock20.png')) //已解锁 } // 更新控件(保持选中状态,所以可以更新) updateControls(); @@ -1272,49 +1272,7 @@ $('#circle_stroke_transparent').click(function () { }) // 清理不需要的 src 字段(保留自定义图片的 src) // cleanupSrcFields 函数已在 output.js 中定义 - -function saveAs(op1, callback) { - const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; - let j = canvas1.toJSON(props) - let b = canvas2.toJSON(props) - - // 清理不需要的 src 字段(使用 window.cleanupSrcFields,在 output.js 中定义) - if (typeof window.cleanupSrcFields === 'function') { - window.cleanupSrcFields(j.objects, objs1); - window.cleanupSrcFields(b.objects, objs2); - } - - let o = { f: j, b: b, fo: objs1, bo: objs2 } - let con_o = Object.assign(o, op1) - con_o.soonType = 2 - var content = JSON.stringify(con_o) - var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); - if (!dialogApi) return; - dialogApi.showSaveDialog({ title: language_str('saveFile'), filters: [{ name: 'Soon File Type', extensions: ['soon'] }] }) - .then(function(result) { - var fp = result.filePath || (result.fileHandle && result.fileHandle.name); - if (!fp && !result.fileHandle) return; - if (typeof fp === 'string' && fp.substring(fp.length - 5).indexOf('.') === -1) fp += '.soon'; - if (window.platformBridge && window.platformBridge.writeFile) { - window.platformBridge.writeFile(result.fileHandle || fp, content).then(function() { - openAs.name = fp; - layer.msg(language_str('saveSucc') + openAs.name); - if (typeof window.saveHistory === 'function') window.saveHistory(); - if (callback && typeof callback === 'function') callback(); - }); - return; - } - var fs = typeof require !== 'undefined' ? require('fs') : null; - if (fs) { - fs.writeFileSync(fp, content, 'utf8'); - openAs.name = fp; - layer.msg(language_str('saveSucc') + openAs.name); - if (typeof window.saveHistory === 'function') window.saveHistory(); - if (callback && typeof callback === 'function') callback(); - } - }) - .catch(function(err) {}); -} +// saveAs 在 output.js 中定义(含辅助线清理);ui 内勿重复定义以免覆盖 // save 函数已在 output.js 中定义 // 复制 $("#copy").click(function () { @@ -1419,7 +1377,7 @@ $('#new').click(function () { function chagneTitle() { } function addBackground() { // 类型改变 - fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { + fabric.Image.fromURL(soonAsset('bg_front_2.png'), function (image) { //image.set("scaleX",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 @@ -1440,7 +1398,7 @@ function addBackground() { recordJson1.push(canvas1.toJSON(props)) }) // 类型改变 - fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { + fabric.Image.fromURL(soonAsset('bg_back_2.png'), function (image) { image.left = ($('#canvas2').width() - image.width) / 2 image.top = ($('#canvas2').height() - image.height) / 2 @@ -1487,9 +1445,9 @@ function updateControls() { $('#text_w').val((activeObj.get('scaleX') * activeObj.get('width')).toFixed(0)) // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) if (activeObj.get('lockMovementX')) { - $('#lock_img').attr('src', 'public/images/lock20.png') //已锁定 + $('#lock_img').attr('src', soonAsset('lock20.png')) //已锁定 } else { - $('#lock_img').attr('src', 'public/images/unlock20.png') //未锁定 + $('#lock_img').attr('src', soonAsset('unlock20.png')) //未锁定 } } function checkName(name) { @@ -1567,7 +1525,7 @@ function selectPic(target, index) { $('#barcode_controll').hide() } function addOutPic(pointer) { - fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) { let width = canvas.getWidth() let height = canvas.getHeight() image.left = pointer.x @@ -2653,7 +2611,9 @@ $('#out_text_font_family').on('change', function () { }) $('.obj_list').on('click', 'div', function () { let idx = objs.length - 1 - $(this).index() - selectObj(idx) + if (typeof window.selectObj === 'function') { + window.selectObj(idx) + } }) $('#exceed').click(function () { let idx = getIndex(canvas.getActiveObject()) @@ -2726,7 +2686,7 @@ $("#about").click(async function () { , id: 'LAY_layuipro' , moveType: 1 , content: '
' + - '
' + + '
' + '
Soon Design
' + // 【修改点】这里使用了刚才获取到的 version 变量 '
' + language_str("vers") + ' ' + version + '
' + @@ -2742,16 +2702,16 @@ $("#help").click(function() { // =========================================================== $('#display').off('click').on('click', function () { // 类型改变 - fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_2.png', function (i1) { + fabric.Image.fromURL(soonAsset('front_bg') + bg_version + '_2.png', function (i1) { i1.left = background_image.left i1.top = background_image.top let img1 = i1 - fabric.Image.fromURL('./public/images/back_bg_2.png', function (i2) { + fabric.Image.fromURL(soonAsset('back_bg_2.png'), function (i2) { i2.left = background_image.left i2.top = background_image.top let img2 = i2 - fabric.Image.fromURL('./public/images/op_2.png', function (i3) { + fabric.Image.fromURL(soonAsset('op_2.png'), function (i3) { i2.left = background_image.left i2.top = background_image.top let img3 = i3 diff --git a/lib/index.js b/frontend-electron/js/index.js similarity index 91% rename from lib/index.js rename to frontend-electron/js/index.js index 1cf1627..db5eb59 100644 --- a/lib/index.js +++ b/frontend-electron/js/index.js @@ -1,4 +1,4 @@ -// 使用平台桥(桌面端由 lib/platform/electron.js 注入 window.ipcRenderer / window.dialog / window.sysAPI) +// 平台桥由 js/platform/electron.js 注入 var ipcRenderer = typeof window !== 'undefined' ? window.ipcRenderer : null; var dialog = typeof window !== 'undefined' ? window.dialog : null; @@ -14,6 +14,17 @@ function escapeAttr(s) { return String(s).replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } +// Electron 历史曾被误写 soondesign_session: 前缀,读取时还原为磁盘路径 +function isWebPortal() { + return !!(window.platformBridge && !ipcRenderer); +} +function resolveDiskPath(p) { + if (!p) return p; + const prefix = 'soondesign_session:'; + if (ipcRenderer && p.indexOf(prefix) === 0) return p.substring(prefix.length); + return p; +} + let s_lan = "zh"; function language_str(str) { @@ -112,7 +123,12 @@ layui.use(['layer', 'form', 'jquery'], function () { let fileExists = true; let imgStyle = ""; let realType = item.type; - const soonData = await window.sysAPI.readJsonFile(item.path); + const diskPath = resolveDiskPath(item.path); + if (diskPath !== item.path) { + item.path = diskPath; + needUpdate = true; + } + const soonData = await window.sysAPI.readJsonFile(diskPath); if (soonData) { src = soonData.frontDisplayPic; let fileType = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1); @@ -124,9 +140,9 @@ layui.use(['layer', 'form', 'jquery'], function () { } else { fileExists = false; if (realType && (realType == 2 || realType == "2")) { - src = "./public/images/bg_2.png"; + src = soonAsset("bg_2.png"); } else { - src = "./public/images/bg_1.png"; + src = soonAsset("bg_1.png"); } imgStyle = "opacity: 0.6; filter: grayscale(100%);"; } @@ -195,7 +211,7 @@ layui.use(['layer', 'form', 'jquery'], function () { , id: 'LAY_layuipro' , moveType: 1 , content: '
' + - '
' + + '
' + '
Soon Design
' + '
' + language_str("vers") + ' ' + version + '
' + '
' + language_str("copyright") + '
' + @@ -207,14 +223,14 @@ layui.use(['layer', 'form', 'jquery'], function () { // 点击事件 (修改后:完全匹配“保存”弹窗样式) // ========================================== $(".card-list").on("click", '.card', async function () { - let filePath = $(this).attr("data-file") || $(this).attr("data"); + let filePath = resolveDiskPath($(this).attr("data-file") || $(this).attr("data")); // 再次检查文件是否存在 const soonData = await window.sysAPI.readJsonFile(filePath); if (soonData) { // 1. 文件存在 -> 正常打开 let type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1); const currentData = await window.sysAPI.readHistory(); - let newHistory = currentData.history.filter(item => item.path !== filePath); + let newHistory = currentData.history.filter(item => resolveDiskPath(item.path) !== filePath); newHistory.unshift({ path: filePath, time: Date.now(), type: type }); await window.sysAPI.writeHistory({ history: newHistory }); @@ -245,7 +261,7 @@ layui.use(['layer', 'form', 'jquery'], function () { shadeClose: true, yes: async function(index){ const currentData = await window.sysAPI.readHistory(); - const newHistory = currentData.history.filter(item => item.path !== filePath); + const newHistory = currentData.history.filter(item => resolveDiskPath(item.path) !== filePath); if (newHistory.length !== currentData.history.length) { await window.sysAPI.writeHistory({ history: newHistory }); @@ -276,7 +292,7 @@ layui.use(['layer', 'form', 'jquery'], function () { filePath = file.name || 'design.soon'; soonData = await window.sysAPI.readJsonFile(file); // 网页端:将内容存到 sessionStorage,设计页通过 sessionKey 读取 - if (soonData && typeof sessionStorage !== 'undefined' && window.platformBridge) { + if (soonData && typeof sessionStorage !== 'undefined' && isWebPortal()) { try { sessionStorage.setItem('soondesign_session:' + filePath, JSON.stringify(soonData)); filePath = 'soondesign_session:' + filePath; @@ -292,7 +308,7 @@ layui.use(['layer', 'form', 'jquery'], function () { } var type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1); var currentData = await window.sysAPI.readHistory(); - var newHistory = currentData.history.filter(function(item) { return item.path !== filePath; }); + var newHistory = currentData.history.filter(function(item) { return resolveDiskPath(item.path) !== filePath; }); newHistory.unshift({ path: filePath, time: Date.now(), type: type }); await window.sysAPI.writeHistory({ history: newHistory }); if (window.platformBridge && window.platformBridge.openDesignPage) window.platformBridge.openDesignPage(filePath, type); diff --git a/frontend-electron/js/platform/bridge.js b/frontend-electron/js/platform/bridge.js new file mode 100644 index 0000000..38491a0 --- /dev/null +++ b/frontend-electron/js/platform/bridge.js @@ -0,0 +1,44 @@ +/** + * 平台抽象层 - 统一接口定义 + * 桌面端由 js/platform/electron.js 实现,网页端由 js/platform/web.js 实现。 + * 业务代码通过 window.platformBridge 或兼容的 window.sysAPI / window.dialog 调用。 + */ +(function () { + 'use strict'; + + // 若已存在(由 electron 或 web 注入),则不覆盖 + if (typeof window.platformBridge !== 'undefined') { + return; + } + + // 默认空实现,避免未注入时报错(仅作占位,实际运行前必须注入 electron 或 web 实现) + function notImplemented() { + console.warn('[platform] bridge not injected'); + return Promise.reject(new Error('Platform bridge not available')); + } + + window.platformBridge = { + readHistory: notImplemented, + writeHistory: notImplemented, + readJsonFile: notImplemented, + showOpenDialog: notImplemented, + showSaveDialog: notImplemented, + writeFile: notImplemented, + readFile: notImplemented, + getAppVersion: function () { return Promise.resolve('0.0.0'); }, + getLocale: function () { return 'zh'; }, + getUserDataPath: function () { return ''; }, + getSystemFonts: function () { return Promise.resolve([]); }, + openDesignPage: function () {}, + openFirstPage: function () {}, + onClose: function () {}, + runClose: function () {}, + openHelp: function () {}, + printPdf: notImplemented, + getScaleRate: function () { return typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1; }, + clipboard: { + readText: function () { return Promise.resolve(''); }, + writeText: function () { return Promise.resolve(); } + } + }; +})(); diff --git a/lib/platform/electron.js b/frontend-electron/js/platform/electron.js similarity index 100% rename from lib/platform/electron.js rename to frontend-electron/js/platform/electron.js diff --git a/main.js b/frontend-electron/main.js similarity index 71% rename from main.js rename to frontend-electron/main.js index 3f0b688..84843c6 100644 --- a/main.js +++ b/frontend-electron/main.js @@ -1,276 +1,261 @@ -const { ipcMain, Menu, shell, app, BrowserWindow, dialog } = require("electron"); -const fs = require('fs'); -const fontList = require("font-list"); -const path = require("path"); -const child_process = require("child_process"); - -console.log(`Node.js: ${process.versions.node}`); - -// 初始化 Remote 模块 -require("@electron/remote/main").initialize(); - -let preFilePath = ""; - -// 1. 定义数据路径(放在全局,方便后续调用) -const exePath = app.getPath('userData'); -const historyFilePath = path.join(exePath, 'data.json'); -console.log('User data path:', exePath); - -app.on("will-finish-launching", () => { - app.on("open-file", (e, filePath) => { - preFilePath = filePath; - }); - if (process.platform === "win32" && process.argv.length >= 2) { - console.log("process argv:", process.argv); - // windows系统当没有路径参数时这个位置默认有个.,需要加以判断 - preFilePath = process.argv[1] === "." ? "" : process.argv[1]; - } -}); - -function createWindow() { - // Create the browser window. - let size = require("electron").screen.getPrimaryDisplay().workAreaSize; - let width = parseInt(size.width); - var mainWindow; - - mainWindow = new BrowserWindow({ - show: false, - width: size.width, - height: size.height, - resizable: false, - icon: "public/images/favicon.ico", // sets window icon - webPreferences: { - preload: path.join(__dirname, "preload.js"), // 确保你有这个文件 - nodeIntegration: true, - contextIsolation: false, // 注意:为了安全建议后续开启,但目前保持你的配置 - enableRemoteModule: true, - }, - }); - - mainWindow.maximize(); - //Menu.setApplicationMenu(null); // 关闭菜单 - - // 启动逻辑:如果有预打开的文件,直接跳转 - if (preFilePath) { - try { - let soonData = fs.readFileSync(preFilePath) - let j = JSON.parse(soonData.toString()); - let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; - mainWindow.loadURL(`file://${__dirname}/design${type}.html?file=${preFilePath}&type=${type}`); - } catch (e) { - console.error("Error loading preFilePath:", e); - mainWindow.loadFile("index.html"); - } - } else { - mainWindow.loadFile("index.html"); - } - - var close_flag = 0; - mainWindow.on("close", function (e) { - if (close_flag == 0) { - e.preventDefault(); - mainWindow.webContents.send("close"); - } - }); - mainWindow.show(); - - // ========================================== - // 新增:安全的文件读写处理 (IPC Handle) - // ========================================== - - // 1. 读取历史记录 - ipcMain.handle('history:read', async () => { - try { - if (!fs.existsSync(historyFilePath)) { - return { history: [] }; - } - const data = fs.readFileSync(historyFilePath, 'utf-8'); - return JSON.parse(data); - } catch (err) { - console.error('主进程读取历史失败:', err); - return { history: [] }; - } - }); - - // 2. 写入历史记录 (包含权限检查) - ipcMain.handle('history:write', async (event, data) => { - try { - // 简单的权限检查 - if (fs.existsSync(historyFilePath)) { - try { - fs.accessSync(historyFilePath, fs.constants.R_OK | fs.constants.W_OK); - } catch (e) { - console.error("无权限写入配置文件"); - return { success: false, error: "PERMISSION_DENIED" }; - } - } - fs.writeFileSync(historyFilePath, JSON.stringify(data)); - return { success: true }; - } catch (err) { - console.error('主进程写入历史失败:', err); - return { success: false, error: err.message }; - } - }); - // 新增:获取软件版本号 - ipcMain.handle('app:get-version', () => { - return app.getVersion(); // 自动读取 package.json 中的 version 字段 - }); - - // 3. 读取 .soon 项目文件 (用于获取缩略图等) - ipcMain.handle('file:read-json', async (event, filePath) => { - try { - if (!fs.existsSync(filePath)) return null; - const data = fs.readFileSync(filePath, 'utf-8'); - return JSON.parse(data); - } catch (err) { - // console.error('读取文件失败:', filePath, err); - return null; - } - }); - - // ========================================== - // 原有的 IPC 监听 - // ========================================== - - ipcMain.on("get-sys-fonts", (event) => { - fontList.getFonts() - .then((fonts) => { - event.reply("font-list", fonts); - }) - .catch((err) => { - console.log(err); - }); - }); - - ipcMain.on("get-sys-language", (event) => { - event.reply("sys-lan", app.getLocale()); - }); - - // 修复后的 Help 文件打开逻辑 - ipcMain.on("open-help-file", async (event) => { - let helpPath; - const fileName = 'User Manual.pdf'; - - if (app.isPackaged) { - // Linux打包后的路径:resources/help/ 或 resources/app.asar/help/ - // Windows/Mac打包后的路径:resources/help/ - if (process.platform === 'linux') { - // Linux上,尝试多个可能的路径 - const possiblePaths = [ - path.join(process.resourcesPath, 'help', fileName), - path.join(process.resourcesPath, 'app.asar', 'help', fileName), - path.join(__dirname, 'help', fileName), - path.join(process.resourcesPath, '..', 'help', fileName) - ]; - - for (const testPath of possiblePaths) { - if (fs.existsSync(testPath)) { - helpPath = testPath; - break; - } - } - } else { - helpPath = path.join(process.resourcesPath, 'help', fileName); - } - } else { - helpPath = path.join(process.cwd(), 'help', fileName); - } - - console.log("Help file path: ", helpPath); - - if (helpPath && fs.existsSync(helpPath)) { - try { - // Linux上使用shell.openExternal更可靠 - if (process.platform === 'linux') { - await shell.openExternal(`file://${helpPath}`); - } else { - const errorMessage = await shell.openPath(helpPath); - if (errorMessage) { - console.error('Error opening help file:', errorMessage); - // 如果openPath失败,尝试openExternal - await shell.openExternal(`file://${helpPath}`); - } - } - console.log('Help file opened successfully'); - } catch (err) { - console.error('Exception when opening file:', err); - // 尝试使用openExternal作为后备方案 - try { - await shell.openExternal(`file://${helpPath}`); - } catch (err2) { - console.error('Failed to open with openExternal:', err2); - } - } - } else { - console.error('Help file not found at:', helpPath); - // 尝试查找help文件的其他可能位置 - const searchPaths = [ - path.join(__dirname, 'help', fileName), - path.join(process.cwd(), 'help', fileName), - path.join(app.getAppPath(), 'help', fileName) - ]; - for (const searchPath of searchPaths) { - if (fs.existsSync(searchPath)) { - console.log('Found help file at alternative path:', searchPath); - try { - if (process.platform === 'linux') { - await shell.openExternal(`file://${searchPath}`); - } else { - await shell.openPath(searchPath); - } - return; - } catch (err) { - console.error('Failed to open help file:', err); - } - } - } - } - }); - - ipcMain.on("print-pdf", (event, pdfData) => { - printPDF(pdfData); - }); - - function printPDF(pdfData) { - const printWindow = new BrowserWindow({ show: true }); // 如果不想显示窗口,改 show: false - printWindow.loadURL(pdfData); - - printWindow.webContents.on("did-finish-load", () => { - printWindow.webContents.print({}); - // printWindow.close(); // 打印通常是异步的,直接关闭可能导致打印失败 - }); - } - - ipcMain.on("open-design-page", (event, arg, type) => { - arg = arg ? arg : 'empty' - type = type ? type : '1' - mainWindow.loadURL(`file://${__dirname}/design${type}.html?file=${arg}&type=${type}`); - }); - - ipcMain.on("open-first-page", (event, arg) => { - mainWindow.loadURL(`file://${__dirname}/index.html`); - }); - - ipcMain.on("run-close", (event, arg) => { - close_flag = 1; - app.quit(); - }); - - require("@electron/remote/main").enable(mainWindow.webContents); -} - -app.commandLine.appendSwitch("no-sandbox"); - -app.whenReady().then(() => { - createWindow(); - // initialize 已经在顶部调用过,这里不需要重复调用,但保留也无害 - // require("@electron/remote/main").initialize(); - - app.on("activate", function () { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); - }); -}); - -app.on("window-all-closed", function () { - if (process.platform !== "darwin") app.quit(); +const { ipcMain, Menu, shell, app, BrowserWindow, dialog } = require("electron"); +const fs = require('fs'); +const fontList = require("font-list"); +const path = require("path"); +const child_process = require("child_process"); + +console.log(`Node.js: ${process.versions.node}`); + +// 初始化 Remote 模块 +require("@electron/remote/main").initialize(); + +let preFilePath = ""; +let mainWindow = null; +let close_flag = 0; +let ipcRegistered = false; + +// 1. 定义数据路径(放在全局,方便后续调用) +const exePath = app.getPath('userData'); +const historyFilePath = path.join(exePath, 'data.json'); +console.log('User data path:', exePath); + +function printPDF(pdfData) { + const printWindow = new BrowserWindow({ show: true }); + printWindow.loadURL(pdfData); + printWindow.webContents.on("did-finish-load", () => { + printWindow.webContents.print({}); + }); +} + +function registerIpcHandlers() { + if (ipcRegistered) return; + ipcRegistered = true; + + ipcMain.handle('history:read', async () => { + try { + if (!fs.existsSync(historyFilePath)) { + return { history: [] }; + } + const data = fs.readFileSync(historyFilePath, 'utf-8'); + return JSON.parse(data); + } catch (err) { + console.error('主进程读取历史失败:', err); + return { history: [] }; + } + }); + + ipcMain.handle('history:write', async (event, data) => { + try { + if (fs.existsSync(historyFilePath)) { + try { + fs.accessSync(historyFilePath, fs.constants.R_OK | fs.constants.W_OK); + } catch (e) { + console.error("无权限写入配置文件"); + return { success: false, error: "PERMISSION_DENIED" }; + } + } + fs.writeFileSync(historyFilePath, JSON.stringify(data)); + return { success: true }; + } catch (err) { + console.error('主进程写入历史失败:', err); + return { success: false, error: err.message }; + } + }); + + ipcMain.handle('app:get-version', () => { + return app.getVersion(); + }); + + ipcMain.handle('file:read-json', async (event, filePath) => { + try { + if (!filePath || !fs.existsSync(filePath)) return null; + const data = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(data); + } catch (err) { + return null; + } + }); + + ipcMain.on("get-sys-fonts", (event) => { + fontList.getFonts() + .then((fonts) => { + event.reply("font-list", fonts); + }) + .catch((err) => { + console.log(err); + }); + }); + + ipcMain.on("get-sys-language", (event) => { + event.reply("sys-lan", app.getLocale()); + }); + + ipcMain.on("open-help-file", async (event) => { + let helpPath; + const fileName = 'User Manual.pdf'; + + if (app.isPackaged) { + if (process.platform === 'linux') { + const possiblePaths = [ + path.join(process.resourcesPath, 'help', fileName), + path.join(process.resourcesPath, 'app.asar', 'help', fileName), + path.join(__dirname, 'assets', 'help', fileName), + path.join(process.resourcesPath, '..', 'help', fileName) + ]; + for (const testPath of possiblePaths) { + if (fs.existsSync(testPath)) { + helpPath = testPath; + break; + } + } + } else { + helpPath = path.join(process.resourcesPath, 'help', fileName); + } + } else { + helpPath = path.join(__dirname, 'assets', 'help', fileName); + } + + console.log("Help file path: ", helpPath); + + if (helpPath && fs.existsSync(helpPath)) { + try { + if (process.platform === 'linux') { + await shell.openExternal(`file://${helpPath}`); + } else { + const errorMessage = await shell.openPath(helpPath); + if (errorMessage) { + console.error('Error opening help file:', errorMessage); + await shell.openExternal(`file://${helpPath}`); + } + } + console.log('Help file opened successfully'); + } catch (err) { + console.error('Exception when opening file:', err); + try { + await shell.openExternal(`file://${helpPath}`); + } catch (err2) { + console.error('Failed to open with openExternal:', err2); + } + } + } else { + console.error('Help file not found at:', helpPath); + const searchPaths = [ + path.join(__dirname, 'assets', 'help', fileName), + path.join(app.getAppPath(), 'help', fileName) + ]; + for (const searchPath of searchPaths) { + if (fs.existsSync(searchPath)) { + console.log('Found help file at alternative path:', searchPath); + try { + if (process.platform === 'linux') { + await shell.openExternal(`file://${searchPath}`); + } else { + await shell.openPath(searchPath); + } + return; + } catch (err) { + console.error('Failed to open help file:', err); + } + } + } + } + }); + + ipcMain.on("print-pdf", (event, pdfData) => { + printPDF(pdfData); + }); + + ipcMain.on("open-design-page", (event, arg, type) => { + if (!mainWindow) return; + arg = arg ? arg : 'empty'; + type = type ? type : '1'; + mainWindow.loadURL(`file://${path.join(__dirname, "pages", "design" + type + ".html")}?file=${encodeURIComponent(arg)}&type=${type}`); + }); + + ipcMain.on("open-first-page", (event, arg) => { + if (!mainWindow) return; + mainWindow.loadURL(`file://${path.join(__dirname, "pages", "index.html")}`); + }); + + ipcMain.on("run-close", (event, arg) => { + close_flag = 1; + app.quit(); + }); +} + +app.on("will-finish-launching", () => { + app.on("open-file", (e, filePath) => { + preFilePath = filePath; + }); + if (process.platform === "win32" && process.argv.length >= 2) { + console.log("process argv:", process.argv); + // windows系统当没有路径参数时这个位置默认有个.,需要加以判断 + preFilePath = process.argv[1] === "." ? "" : process.argv[1]; + } +}); + +function createWindow() { + registerIpcHandlers(); + close_flag = 0; + + let size = require("electron").screen.getPrimaryDisplay().workAreaSize; + + mainWindow = new BrowserWindow({ + show: false, + width: size.width, + height: size.height, + resizable: false, + icon: path.join(__dirname, "assets/images/favicon.ico"), + webPreferences: { + preload: path.join(__dirname, "preload.js"), // 确保你有这个文件 + nodeIntegration: true, + contextIsolation: false, // 注意:为了安全建议后续开启,但目前保持你的配置 + enableRemoteModule: true, + }, + }); + + mainWindow.maximize(); + //Menu.setApplicationMenu(null); // 关闭菜单 + + // 启动逻辑:如果有预打开的文件,直接跳转 + if (preFilePath) { + try { + let soonData = fs.readFileSync(preFilePath) + let j = JSON.parse(soonData.toString()); + let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + mainWindow.loadURL(`file://${path.join(__dirname, "pages", "design" + type + ".html")}?file=${encodeURIComponent(preFilePath)}&type=${type}`); + } catch (e) { + console.error("Error loading preFilePath:", e); + mainWindow.loadFile(path.join(__dirname, "pages", "index.html")); + } + } else { + mainWindow.loadFile(path.join(__dirname, "pages", "index.html")); + } + + var closeHandler = function (e) { + if (close_flag == 0) { + e.preventDefault(); + mainWindow.webContents.send("close"); + } + }; + mainWindow.on("close", closeHandler); + mainWindow.show(); + + require("@electron/remote/main").enable(mainWindow.webContents); +} + +app.commandLine.appendSwitch("no-sandbox"); + +app.whenReady().then(() => { + createWindow(); + // initialize 已经在顶部调用过,这里不需要重复调用,但保留也无害 + // require("@electron/remote/main").initialize(); + + app.on("activate", function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on("window-all-closed", function () { + if (process.platform !== "darwin") app.quit(); }); \ No newline at end of file diff --git a/package arm64.json b/frontend-electron/package arm64.json similarity index 100% rename from package arm64.json rename to frontend-electron/package arm64.json diff --git a/frontend-electron/package-lock.json b/frontend-electron/package-lock.json new file mode 100644 index 0000000..f889cd0 --- /dev/null +++ b/frontend-electron/package-lock.json @@ -0,0 +1,5704 @@ +{ + "name": "soondesign", + "version": "3.2.101", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "soondesign", + "version": "3.2.101", + "license": "MIT", + "dependencies": { + "@electron/remote": "^2.0.8", + "buffer": "^6.0.3", + "font-list": "^1.4.2", + "jr-qrcode": "^1.1.4", + "jsbarcode": "^3.11.5", + "less": "^4.1.2", + "less-loader": "^10.2.0", + "save": "^2.9.0" + }, + "devDependencies": { + "app-builder-bin": "^4.2.0", + "electron": "^15.2.0", + "electron-builder": "^23.3.3", + "electron-packager": "^15.5.2", + "webpack": "^5.96.1", + "webpack-cli": "^5.1.4" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@electron/get/-/get-1.14.1.tgz", + "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^9.6.0", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=8.6" + }, + "optionalDependencies": { + "global-agent": "^3.0.0", + "global-tunnel-ng": "^2.7.1" + } + }, + "node_modules/@electron/remote": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@electron/remote/-/remote-2.1.3.tgz", + "integrity": "sha512-XlpxC8S4ttj/v2d+PKp9na/3Ev8bV7YWNL7Cw5b9MAWgTphEml7iYgbc7V0r9D6yDOfOkj06bchZgOZdlWJGNA==", + "license": "MIT", + "peerDependencies": { + "electron": ">= 13.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmmirror.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-4.2.0.tgz", + "integrity": "sha512-PGXlkukQnroTgAaDZnnppdLzsRJmab6Rh/rJ5fKyYaYhd+FfaORH59/ArkB5dr2cAeYQU5lCeHFEwURaoBO8BA==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "23.6.0", + "resolved": "https://registry.npmmirror.com/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/universal": "1.2.1", + "@malept/flatpak-bundler": "^0.4.0", + "7zip-bin": "~5.1.1", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^4.0.10", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^3.1.2", + "read-config-file": "6.2.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.7", + "tar": "^6.1.11", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asar": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", + "dev": true, + "license": "MIT", + "dependencies": { + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + }, + "optionalDependencies": { + "@types/glob": "^7.1.1" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "23.6.0", + "resolved": "https://registry.npmmirror.com/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "@types/fs-extra": "^9.0.11", + "7zip-bin": "~5.1.1", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.1.1", + "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmmirror.com/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/dir-compare/-/dir-compare-2.4.0.tgz", + "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "1.0.0", + "colors": "1.0.3", + "commander": "2.9.0", + "minimatch": "3.0.4" + }, + "bin": { + "dircompare": "src/cli/dircompare.js" + } + }, + "node_modules/dir-compare/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmmirror.com/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "license": "BSD-3-Clause" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/electron/-/electron-15.5.7.tgz", + "integrity": "sha512-n4mVlxoMc4eYx07wWFWGficL+iOMz5xZEf5dBtE/wwLm0fQpYVyW4AlknMFG9F8Css0MM0JSwNMOyRg5e1vDtg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^1.13.0", + "@types/node": "^14.6.2", + "extract-zip": "^1.0.3" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 8.6" + } + }, + "node_modules/electron-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmmirror.com/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs": "^17.0.1", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "dmg-builder": "23.6.0", + "fs-extra": "^10.0.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.2.0", + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-notarize": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/electron-notarize/-/electron-notarize-1.2.2.tgz", + "integrity": "sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw==", + "deprecated": "Please use @electron/notarize moving forward. There is no API change, just a package name change", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-osx-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", + "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^3.0.1" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-osx-sign/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-osx-sign/node_modules/isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/electron-osx-sign/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-packager": { + "version": "15.5.2", + "resolved": "https://registry.npmmirror.com/electron-packager/-/electron-packager-15.5.2.tgz", + "integrity": "sha512-8zUdkSONn0jomu/efqoxApGzgqIb56ooMs671HeB/BXTPnWcWvqpEY08g16PL6ok2ymA5zPj8vmUszLrq99F0Q==", + "deprecated": "Please use @electron/packager moving forward. There is no API change, just a package name change", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@electron/get": "^1.6.0", + "@electron/universal": "^1.2.1", + "asar": "^3.1.0", + "cross-spawn-windows-exe": "^1.2.0", + "debug": "^4.0.1", + "electron-notarize": "^1.1.1", + "electron-osx-sign": "^0.5.0", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^10.1.0", + "galactus": "^0.2.1", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "rcedit": "^3.0.1", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^20.2.9" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 10.12.0" + }, + "funding": { + "url": "https://github.com/electron/electron-packager?sponsor=1" + } + }, + "node_modules/electron-packager/node_modules/electron-osx-sign": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/electron-osx-sign/-/electron-osx-sign-0.5.0.tgz", + "integrity": "sha512-icoRLHzFz/qxzDh/N4Pi2z4yVHurlsCAYQvsCSG7fCedJ4UJXBS6PoQyGH71IfcqKupcKeK7HX/NkyfG+v6vlQ==", + "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^3.0.1" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-packager/node_modules/electron-osx-sign/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-packager/node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/electron-packager/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-packager/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-packager/node_modules/isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/electron-packager/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-packager/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-packager/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "23.6.0", + "resolved": "https://registry.npmmirror.com/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "fs-extra": "^10.0.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.2", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.22.2.tgz", + "integrity": "sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flora-colossus": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/flora-colossus/-/flora-colossus-1.0.1.tgz", + "integrity": "sha512-d+9na7t9FyH8gBJoNDSi28mE4NgQVGGvxQ4aHtFRetjyh5SXjuus+V5EZaxFmFdXVemSOrx0lsgEl/ZMjnOWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^7.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/flora-colossus/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/font-list": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/font-list/-/font-list-1.6.0.tgz", + "integrity": "sha512-gI6N38fcq4er2BkKeW33SQt5nLMGX/GuT7mgWa3OiOZSQAJ3b6ZiCg3hPqlnnaULW/pcR1R7sUMrN6HfovXKAg==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/galactus": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/galactus/-/galactus-0.2.1.tgz", + "integrity": "sha512-mDc8EQJKtxjp9PMYS3PbpjjbX3oXhBTxoGaPahw620XZBIHJ4+nvw5KN/tRtmmSDR9dypstGNvqQ3C29QGoGHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "flora-colossus": "^1.0.0", + "fs-extra": "^4.0.0" + } + }, + "node_modules/galactus/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/galactus/node_modules/fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmmirror.com/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jr-qrcode": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/jr-qrcode/-/jr-qrcode-1.2.1.tgz", + "integrity": "sha512-RXv5g2bIiLtkYDa6EcrE2gJC7dvLohNygrsrQ7+2UizBXeQKZ2UQd8owf0OHt+UXt6CFEg0xABxVf2axErzHlA==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbarcode": { + "version": "3.12.3", + "resolved": "https://registry.npmmirror.com/jsbarcode/-/jsbarcode-3.12.3.tgz", + "integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==", + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/less": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/less/-/less-4.6.4.tgz", + "integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==", + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^3.0.5", + "parse-node-version": "^1.0.1" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/less-loader/-/less-loader-10.2.0.tgz", + "integrity": "sha512-AV5KHWvCezW27GT90WATaDnfXBv99llDbtaj4bshq6DvAihMdNjaPDcUMa6EXKLRF+P2opFenJp89BXg91XLYg==", + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "license": "MIT" + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mingo": { + "version": "6.7.2", + "resolved": "https://registry.npmmirror.com/mingo/-/mingo-6.7.2.tgz", + "integrity": "sha512-5HKLu2GiNxw71EYZbWPh6dpcucHLlHqOXa0gbsYYtAjRpT/8EbDBJURLlUt3cNkISd9grOJ/AeYogyWj3QoxGA==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "license": "MIT", + "optional": true, + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-conf/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmmirror.com/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC", + "optional": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "license": "MIT", + "optional": true + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rcedit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/rcedit/-/rcedit-3.1.0.tgz", + "integrity": "sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/read-config-file": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/read-config-file/-/read-config-file-6.2.0.tgz", + "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/save": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/save/-/save-2.9.0.tgz", + "integrity": "sha512-eg8+g8CjvehE/2C6EbLdtK1pINVD27pcJLj4M9PjWWhoeha/y5bWf4dp/0RF+OzbKTcG1bae9qi3PAqiR8CJTg==", + "license": "ISC", + "dependencies": { + "async": "^3.2.2", + "event-stream": "^4.0.1", + "lodash.assign": "^4.2.0", + "mingo": "^6.1.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "devOptional": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.107.2", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/frontend-electron/package.json similarity index 98% rename from package.json rename to frontend-electron/package.json index 62bd944..f881192 100644 --- a/package.json +++ b/frontend-electron/package.json @@ -26,7 +26,7 @@ }, "electronPackagerConfig": { "packageManager": "npm", - "icon": "./public/images/favicon.ico" + "icon": "./assets/images/favicon.ico" }, "build": { "productName": "SoonDesign", diff --git a/design1.html b/frontend-electron/pages/design1.html similarity index 91% rename from design1.html rename to frontend-electron/pages/design1.html index 4fdca93..02ed2c2 100644 --- a/design1.html +++ b/frontend-electron/pages/design1.html @@ -5,8 +5,8 @@ Soon Design - - + + - - - - - - - - - - - - - - + + + + + + + Soon Design + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/frontend-electron/pages/index.html similarity index 93% rename from index.html rename to frontend-electron/pages/index.html index f5ec59a..63b98e6 100644 --- a/index.html +++ b/frontend-electron/pages/index.html @@ -1,239 +1,241 @@ - - - - - - - SoonDesign - - - - - - -
-
-
-
-

-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- - -
-
-
-
- -
-
-
- - - - + + + + + + + SoonDesign + + + + + + +
+
+
+
+

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/preload.js b/frontend-electron/preload.js similarity index 97% rename from preload.js rename to frontend-electron/preload.js index 247535b..7abda39 100644 --- a/preload.js +++ b/frontend-electron/preload.js @@ -1,29 +1,29 @@ -const { ipcRenderer } = require('electron') - -// 1. 显示版本号逻辑 (保持不变) -window.addEventListener('DOMContentLoaded', () => { - const replaceText = (selector, text) => { - const element = document.getElementById(selector) - if (element) element.innerText = text - } - - for (const type of ['chrome', 'node', 'electron']) { - replaceText(`${type}-version`, process.versions[type]) - } -}) - -// 2. 修正:直接挂载到 window 对象 -// 因为你的 main.js 中 contextIsolation: false,所以直接赋值即可 -window.sysAPI = { - // 读取历史记录 - readHistory: () => ipcRenderer.invoke('history:read'), - - // 写入历史记录 - writeHistory: (data) => ipcRenderer.invoke('history:write', data), - - // 读取任意 JSON 文件 - readJsonFile: (filePath) => ipcRenderer.invoke('file:read-json', filePath), - //获取软件版本 - getAppVersion: () => ipcRenderer.invoke('app:get-version') - +const { ipcRenderer } = require('electron') + +// 1. 显示版本号逻辑 (保持不变) +window.addEventListener('DOMContentLoaded', () => { + const replaceText = (selector, text) => { + const element = document.getElementById(selector) + if (element) element.innerText = text + } + + for (const type of ['chrome', 'node', 'electron']) { + replaceText(`${type}-version`, process.versions[type]) + } +}) + +// 2. 修正:直接挂载到 window 对象 +// 因为你的 main.js 中 contextIsolation: false,所以直接赋值即可 +window.sysAPI = { + // 读取历史记录 + readHistory: () => ipcRenderer.invoke('history:read'), + + // 写入历史记录 + writeHistory: (data) => ipcRenderer.invoke('history:write', data), + + // 读取任意 JSON 文件 + readJsonFile: (filePath) => ipcRenderer.invoke('file:read-json', filePath), + //获取软件版本 + getAppVersion: () => ipcRenderer.invoke('app:get-version') + }; \ No newline at end of file diff --git a/public/js/JsBarcode.all.min.js b/frontend-electron/vendor/js/JsBarcode.all.min.js similarity index 100% rename from public/js/JsBarcode.all.min.js rename to frontend-electron/vendor/js/JsBarcode.all.min.js diff --git a/public/js/crypto-js.min.js b/frontend-electron/vendor/js/crypto-js.min.js similarity index 100% rename from public/js/crypto-js.min.js rename to frontend-electron/vendor/js/crypto-js.min.js diff --git a/public/js/fabric.min.js b/frontend-electron/vendor/js/fabric.min.js similarity index 100% rename from public/js/fabric.min.js rename to frontend-electron/vendor/js/fabric.min.js diff --git a/public/js/fabric1.min.js b/frontend-electron/vendor/js/fabric1.min.js similarity index 100% rename from public/js/fabric1.min.js rename to frontend-electron/vendor/js/fabric1.min.js diff --git a/public/js/fabric2.min.js b/frontend-electron/vendor/js/fabric2.min.js similarity index 100% rename from public/js/fabric2.min.js rename to frontend-electron/vendor/js/fabric2.min.js diff --git a/public/js/guideLines.js b/frontend-electron/vendor/js/guideLines.js similarity index 97% rename from public/js/guideLines.js rename to frontend-electron/vendor/js/guideLines.js index 91080b2..d3e59de 100644 --- a/public/js/guideLines.js +++ b/frontend-electron/vendor/js/guideLines.js @@ -1,219 +1,219 @@ -function initAligningGuidelines(canvas) { - - var ctx = canvas.getSelectionContext(), - //aligningLineOffset = 0, - //aligningLineMargin = 0, - //aligningLineWidth = 1, - //aligningLineColor = 'rgb(0,0,0)', - aligningLineOffset = 4, - aligningLineMargin = 5,//吸引 - aligningLineWidth = 6, - aligningLineColor = '#6637FF', - viewportTransform, - zoom = 1; - - function drawVerticalLine(coords) { - drawLine( - coords.x + 0.5, - coords.y1 > coords.y2 ? coords.y2 : coords.y1, - coords.x + 0.5, - coords.y2 > coords.y1 ? coords.y2 : coords.y1); - } - - function drawHorizontalLine(coords) { - drawLine( - coords.x1 > coords.x2 ? coords.x2 : coords.x1, - coords.y + 0.5, - coords.x2 > coords.x1 ? coords.x2 : coords.x1, - coords.y + 0.5); - } - - function drawLine(x1, y1, x2, y2) { - ctx.save(); - ctx.lineWidth = aligningLineWidth; - ctx.strokeStyle = aligningLineColor; - ctx.beginPath(); - let width = canvas.getWidth(); - let height = canvas.getHeight(); - ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 - ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 - //修复中心放大后的位置偏移 2021-10-24 - ctx.stroke(); - ctx.restore(); - } - - function isInRange(value1, value2) { - value1 = Math.round(value1); - value2 = Math.round(value2); - for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { - if (i === value2) { - return true; - } - } - return false; - } - - var verticalLines = [], - horizontalLines = []; - - canvas.on('mouse:down', function () { - viewportTransform = canvas.viewportTransform; - zoom = canvas.getZoom(); - }); - - canvas.on('object:moving', function(e) { - - var activeObject = e.target, - canvasObjects = canvas.getObjects(), - activeObjectCenter = activeObject.getCenterPoint(), - activeObjectLeft = activeObjectCenter.x, - activeObjectTop = activeObjectCenter.y, - activeObjectBoundingRect = activeObject.getBoundingRect(), - activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], - activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], - horizontalInTheRange = false, - verticalInTheRange = false, - transform = canvas._currentTransform; - - if (!transform) return; - - // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, - // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move - - for (var i = canvasObjects.length; i--; ) { - - if (canvasObjects[i] === activeObject) continue; - - var objectCenter = canvasObjects[i].getCenterPoint(), - objectLeft = objectCenter.x, - objectTop = objectCenter.y, - objectBoundingRect = canvasObjects[i].getBoundingRect(), - objectHeight = objectBoundingRect.height / viewportTransform[3], - objectWidth = objectBoundingRect.width / viewportTransform[0]; - - // snap by the horizontal center line - if (isInRange(objectLeft, activeObjectLeft)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); - } - - // snap by the left edge - if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft - objectWidth / 2, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); - } - - // snap by the right edge - if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft + objectWidth / 2, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); - } - - // snap by the vertical center line - if (isInRange(objectTop, activeObjectTop)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); - } - - // snap by the top edge - if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop - objectHeight / 2, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); - } - - // snap by the bottom edge - if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop + objectHeight / 2, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); - } - } - - if (!horizontalInTheRange) { - horizontalLines.length = 0; - } - - if (!verticalInTheRange) { - verticalLines.length = 0; - } - }); - - canvas.on('before:render', function() { - - if(canvas&&canvas.contextTop) - { - canvas.clearContext(canvas.contextTop); - } - - - }); - - canvas.on('after:render', function() { - for (var i = verticalLines.length; i--; ) { - drawVerticalLine(verticalLines[i]); - } - for (var i = horizontalLines.length; i--; ) { - drawHorizontalLine(horizontalLines[i]); - } - verticalLines.length = horizontalLines.length = 0; - }); - - canvas.on('mouse:up', function() { - verticalLines.length = horizontalLines.length = 0; - canvas.renderAll(); - }); -} - +function initAligningGuidelines(canvas) { + + var ctx = canvas.getSelectionContext(), + //aligningLineOffset = 0, + //aligningLineMargin = 0, + //aligningLineWidth = 1, + //aligningLineColor = 'rgb(0,0,0)', + aligningLineOffset = 4, + aligningLineMargin = 5,//吸引 + aligningLineWidth = 6, + aligningLineColor = '#6637FF', + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + let width = canvas.getWidth(); + let height = canvas.getHeight(); + ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 + ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 + //修复中心放大后的位置偏移 2021-10-24 + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on('mouse:down', function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on('object:moving', function(e) { + + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + + if (canvasObjects[i] === activeObject) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + } + + // snap by the left edge + if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the right edge + if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + } + + // snap by the top edge + if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + } + + // snap by the bottom edge + if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on('before:render', function() { + + if(canvas&&canvas.contextTop) + { + canvas.clearContext(canvas.contextTop); + } + + + }); + + canvas.on('after:render', function() { + for (var i = verticalLines.length; i--; ) { + drawVerticalLine(verticalLines[i]); + } + for (var i = horizontalLines.length; i--; ) { + drawHorizontalLine(horizontalLines[i]); + } + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on('mouse:up', function() { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + diff --git a/public/js/guideLines1.js b/frontend-electron/vendor/js/guideLines1.js similarity index 97% rename from public/js/guideLines1.js rename to frontend-electron/vendor/js/guideLines1.js index eaf3694..cb03b84 100644 --- a/public/js/guideLines1.js +++ b/frontend-electron/vendor/js/guideLines1.js @@ -1,255 +1,255 @@ -function initAligningGuidelines(canvas) { - - var ctx = canvas.getSelectionContext(), - //aligningLineOffset = 0, - //aligningLineMargin = 0, - //aligningLineWidth = 1, - //aligningLineColor = 'rgb(0,0,0)', - aligningLineOffset = 4, - aligningLineMargin = 5,//吸引 - aligningLineWidth = 6, - aligningLineColor = '#6637FF', - viewportTransform, - zoom = 1; - - function drawVerticalLine(coords) { - drawLine( - coords.x + 0.5, - coords.y1 > coords.y2 ? coords.y2 : coords.y1, - coords.x + 0.5, - coords.y2 > coords.y1 ? coords.y2 : coords.y1); - } - - function drawHorizontalLine(coords) { - drawLine( - coords.x1 > coords.x2 ? coords.x2 : coords.x1, - coords.y + 0.5, - coords.x2 > coords.x1 ? coords.x2 : coords.x1, - coords.y + 0.5); - } - - function drawLine(x1, y1, x2, y2) { - ctx.save(); - ctx.lineWidth = aligningLineWidth; - ctx.strokeStyle = aligningLineColor; - ctx.beginPath(); - let width = canvas.getWidth(); - let height = canvas.getHeight(); - ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 - ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 - //修复中心放大后的位置偏移 2021-10-24 - ctx.stroke(); - ctx.restore(); - } - - function isInRange(value1, value2) { - value1 = Math.round(value1); - value2 = Math.round(value2); - for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { - if (i === value2) { - return true; - } - } - return false; - } - - var verticalLines = [], - horizontalLines = []; - - canvas.on('mouse:down', function () { - viewportTransform = canvas.viewportTransform; - zoom = canvas.getZoom(); - }); - - canvas.on('object:moving', function(e) { - - var activeObject = e.target, - canvasObjects = canvas.getObjects(), - activeObjectCenter = activeObject.getCenterPoint(), - activeObjectLeft = activeObjectCenter.x, - activeObjectTop = activeObjectCenter.y, - activeObjectBoundingRect = activeObject.getBoundingRect(), - activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], - activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], - horizontalInTheRange = false, - verticalInTheRange = false, - transform = canvas._currentTransform; - - if (!transform) return; - - // 清空辅助线数组,避免累积 - verticalLines.length = 0; - horizontalLines.length = 0; - - // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, - // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move - - for (var i = canvasObjects.length; i--; ) { - - if (canvasObjects[i] === activeObject) continue; - - // 跳过辅助线对象,避免辅助线之间相互对齐产生重影 - if (canvasObjects[i].isGuideLine) continue; - - var objectCenter = canvasObjects[i].getCenterPoint(), - objectLeft = objectCenter.x, - objectTop = objectCenter.y, - objectBoundingRect = canvasObjects[i].getBoundingRect(), - objectHeight = objectBoundingRect.height / viewportTransform[3], - objectWidth = objectBoundingRect.width / viewportTransform[0]; - - // snap by the horizontal center line - if (isInRange(objectLeft, activeObjectLeft)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); - } - - // snap by the left edge - if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft - objectWidth / 2, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); - } - - // snap by the right edge - if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft + objectWidth / 2, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); - } - - // snap by the vertical center line - if (isInRange(objectTop, activeObjectTop)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); - } - - // snap by the top edge - if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop - objectHeight / 2, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); - } - - // snap by the bottom edge - if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop + objectHeight / 2, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); - } - } - - if (!horizontalInTheRange) { - horizontalLines.length = 0; - } - - if (!verticalInTheRange) { - verticalLines.length = 0; - } - }); - - canvas.on('before:render', function() { - - if(canvas&&canvas.contextTop) - { - canvas.clearContext(canvas.contextTop); - } - - - }); - - canvas.on('after:render', function() { - // 合并相同 x 坐标的竖向辅助线(使用四舍五入处理浮点数精度问题) - var mergedVerticalLines = {}; - for (var i = verticalLines.length; i--; ) { - var line = verticalLines[i]; - // 使用四舍五入到整数作为 key,避免浮点数精度问题 - var key = Math.round(line.x); - if (!mergedVerticalLines[key]) { - mergedVerticalLines[key] = { x: line.x, y1: line.y1, y2: line.y2 }; - } else { - // 合并 y 范围 - mergedVerticalLines[key].y1 = Math.min(mergedVerticalLines[key].y1, line.y1); - mergedVerticalLines[key].y2 = Math.max(mergedVerticalLines[key].y2, line.y2); - } - } - for (var key in mergedVerticalLines) { - drawVerticalLine(mergedVerticalLines[key]); - } - - // 合并相同 y 坐标的横向辅助线(使用四舍五入处理浮点数精度问题) - var mergedHorizontalLines = {}; - for (var i = horizontalLines.length; i--; ) { - var line = horizontalLines[i]; - // 使用四舍五入到整数作为 key,避免浮点数精度问题 - var key = Math.round(line.y); - if (!mergedHorizontalLines[key]) { - mergedHorizontalLines[key] = { y: line.y, x1: line.x1, x2: line.x2 }; - } else { - // 合并 x 范围 - mergedHorizontalLines[key].x1 = Math.min(mergedHorizontalLines[key].x1, line.x1); - mergedHorizontalLines[key].x2 = Math.max(mergedHorizontalLines[key].x2, line.x2); - } - } - for (var key in mergedHorizontalLines) { - drawHorizontalLine(mergedHorizontalLines[key]); - } - verticalLines.length = horizontalLines.length = 0; - }); - - canvas.on('mouse:up', function() { - verticalLines.length = horizontalLines.length = 0; - canvas.renderAll(); - }); -} - +function initAligningGuidelines(canvas) { + + var ctx = canvas.getSelectionContext(), + //aligningLineOffset = 0, + //aligningLineMargin = 0, + //aligningLineWidth = 1, + //aligningLineColor = 'rgb(0,0,0)', + aligningLineOffset = 4, + aligningLineMargin = 5,//吸引 + aligningLineWidth = 6, + aligningLineColor = '#6637FF', + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + let width = canvas.getWidth(); + let height = canvas.getHeight(); + ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 + ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 + //修复中心放大后的位置偏移 2021-10-24 + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on('mouse:down', function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on('object:moving', function(e) { + + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // 清空辅助线数组,避免累积 + verticalLines.length = 0; + horizontalLines.length = 0; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + + if (canvasObjects[i] === activeObject) continue; + + // 跳过辅助线对象,避免辅助线之间相互对齐产生重影 + if (canvasObjects[i].isGuideLine) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + } + + // snap by the left edge + if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the right edge + if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + } + + // snap by the top edge + if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + } + + // snap by the bottom edge + if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on('before:render', function() { + + if(canvas&&canvas.contextTop) + { + canvas.clearContext(canvas.contextTop); + } + + + }); + + canvas.on('after:render', function() { + // 合并相同 x 坐标的竖向辅助线(使用四舍五入处理浮点数精度问题) + var mergedVerticalLines = {}; + for (var i = verticalLines.length; i--; ) { + var line = verticalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.x); + if (!mergedVerticalLines[key]) { + mergedVerticalLines[key] = { x: line.x, y1: line.y1, y2: line.y2 }; + } else { + // 合并 y 范围 + mergedVerticalLines[key].y1 = Math.min(mergedVerticalLines[key].y1, line.y1); + mergedVerticalLines[key].y2 = Math.max(mergedVerticalLines[key].y2, line.y2); + } + } + for (var key in mergedVerticalLines) { + drawVerticalLine(mergedVerticalLines[key]); + } + + // 合并相同 y 坐标的横向辅助线(使用四舍五入处理浮点数精度问题) + var mergedHorizontalLines = {}; + for (var i = horizontalLines.length; i--; ) { + var line = horizontalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.y); + if (!mergedHorizontalLines[key]) { + mergedHorizontalLines[key] = { y: line.y, x1: line.x1, x2: line.x2 }; + } else { + // 合并 x 范围 + mergedHorizontalLines[key].x1 = Math.min(mergedHorizontalLines[key].x1, line.x1); + mergedHorizontalLines[key].x2 = Math.max(mergedHorizontalLines[key].x2, line.x2); + } + } + for (var key in mergedHorizontalLines) { + drawHorizontalLine(mergedHorizontalLines[key]); + } + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on('mouse:up', function() { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + diff --git a/public/js/guideLines2.js b/frontend-electron/vendor/js/guideLines2.js similarity index 97% rename from public/js/guideLines2.js rename to frontend-electron/vendor/js/guideLines2.js index 86498f5..552fc5b 100644 --- a/public/js/guideLines2.js +++ b/frontend-electron/vendor/js/guideLines2.js @@ -1,255 +1,255 @@ -function initAligningGuidelines(canvas) { - - var ctx = canvas.getSelectionContext(), - //aligningLineOffset = 0, - //aligningLineMargin = 0, - //aligningLineWidth = 1, - //aligningLineColor = 'rgb(0,0,0)', - aligningLineOffset = 4, - aligningLineMargin = 5,//吸引 - aligningLineWidth = 2, - aligningLineColor = '#6637FF', - viewportTransform, - zoom = 1; - - function drawVerticalLine(coords) { - drawLine( - coords.x + 0.5, - coords.y1 > coords.y2 ? coords.y2 : coords.y1, - coords.x + 0.5, - coords.y2 > coords.y1 ? coords.y2 : coords.y1); - } - - function drawHorizontalLine(coords) { - drawLine( - coords.x1 > coords.x2 ? coords.x2 : coords.x1, - coords.y + 0.5, - coords.x2 > coords.x1 ? coords.x2 : coords.x1, - coords.y + 0.5); - } - - function drawLine(x1, y1, x2, y2) { - ctx.save(); - ctx.lineWidth = aligningLineWidth; - ctx.strokeStyle = aligningLineColor; - ctx.beginPath(); - let width = canvas.getWidth(); - let height = canvas.getHeight(); - ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 - ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 - //修复中心放大后的位置偏移 2021-10-24 - ctx.stroke(); - ctx.restore(); - } - - function isInRange(value1, value2) { - value1 = Math.round(value1); - value2 = Math.round(value2); - for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { - if (i === value2) { - return true; - } - } - return false; - } - - var verticalLines = [], - horizontalLines = []; - - canvas.on('mouse:down', function () { - viewportTransform = canvas.viewportTransform; - zoom = canvas.getZoom(); - }); - - canvas.on('object:moving', function(e) { - - var activeObject = e.target, - canvasObjects = canvas.getObjects(), - activeObjectCenter = activeObject.getCenterPoint(), - activeObjectLeft = activeObjectCenter.x, - activeObjectTop = activeObjectCenter.y, - activeObjectBoundingRect = activeObject.getBoundingRect(), - activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], - activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], - horizontalInTheRange = false, - verticalInTheRange = false, - transform = canvas._currentTransform; - - if (!transform) return; - - // 清空辅助线数组,避免累积 - verticalLines.length = 0; - horizontalLines.length = 0; - - // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, - // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move - - for (var i = canvasObjects.length; i--; ) { - - if (canvasObjects[i] === activeObject) continue; - - // 跳过辅助线对象,避免辅助线之间相互对齐产生重影 - if (canvasObjects[i].isGuideLine) continue; - - var objectCenter = canvasObjects[i].getCenterPoint(), - objectLeft = objectCenter.x, - objectTop = objectCenter.y, - objectBoundingRect = canvasObjects[i].getBoundingRect(), - objectHeight = objectBoundingRect.height / viewportTransform[3], - objectWidth = objectBoundingRect.width / viewportTransform[0]; - - // snap by the horizontal center line - if (isInRange(objectLeft, activeObjectLeft)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); - } - - // snap by the left edge - if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft - objectWidth / 2, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); - } - - // snap by the right edge - if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { - verticalInTheRange = true; - verticalLines.push({ - x: objectLeft + objectWidth / 2, - y1: (objectTop < activeObjectTop) - ? (objectTop - objectHeight / 2 - aligningLineOffset) - : (objectTop + objectHeight / 2 + aligningLineOffset), - y2: (activeObjectTop > objectTop) - ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) - : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); - } - - // snap by the vertical center line - if (isInRange(objectTop, activeObjectTop)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); - } - - // snap by the top edge - if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop - objectHeight / 2, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); - } - - // snap by the bottom edge - if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { - horizontalInTheRange = true; - horizontalLines.push({ - y: objectTop + objectHeight / 2, - x1: (objectLeft < activeObjectLeft) - ? (objectLeft - objectWidth / 2 - aligningLineOffset) - : (objectLeft + objectWidth / 2 + aligningLineOffset), - x2: (activeObjectLeft > objectLeft) - ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) - : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) - }); - activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); - } - } - - if (!horizontalInTheRange) { - horizontalLines.length = 0; - } - - if (!verticalInTheRange) { - verticalLines.length = 0; - } - }); - - canvas.on('before:render', function() { - - if(canvas&&canvas.contextTop) - { - canvas.clearContext(canvas.contextTop); - } - - - }); - - canvas.on('after:render', function() { - // 合并相同 x 坐标的竖向辅助线(使用四舍五入处理浮点数精度问题) - var mergedVerticalLines = {}; - for (var i = verticalLines.length; i--; ) { - var line = verticalLines[i]; - // 使用四舍五入到整数作为 key,避免浮点数精度问题 - var key = Math.round(line.x); - if (!mergedVerticalLines[key]) { - mergedVerticalLines[key] = { x: line.x, y1: line.y1, y2: line.y2 }; - } else { - // 合并 y 范围 - mergedVerticalLines[key].y1 = Math.min(mergedVerticalLines[key].y1, line.y1); - mergedVerticalLines[key].y2 = Math.max(mergedVerticalLines[key].y2, line.y2); - } - } - for (var key in mergedVerticalLines) { - drawVerticalLine(mergedVerticalLines[key]); - } - - // 合并相同 y 坐标的横向辅助线(使用四舍五入处理浮点数精度问题) - var mergedHorizontalLines = {}; - for (var i = horizontalLines.length; i--; ) { - var line = horizontalLines[i]; - // 使用四舍五入到整数作为 key,避免浮点数精度问题 - var key = Math.round(line.y); - if (!mergedHorizontalLines[key]) { - mergedHorizontalLines[key] = { y: line.y, x1: line.x1, x2: line.x2 }; - } else { - // 合并 x 范围 - mergedHorizontalLines[key].x1 = Math.min(mergedHorizontalLines[key].x1, line.x1); - mergedHorizontalLines[key].x2 = Math.max(mergedHorizontalLines[key].x2, line.x2); - } - } - for (var key in mergedHorizontalLines) { - drawHorizontalLine(mergedHorizontalLines[key]); - } - verticalLines.length = horizontalLines.length = 0; - }); - - canvas.on('mouse:up', function() { - verticalLines.length = horizontalLines.length = 0; - canvas.renderAll(); - }); -} - +function initAligningGuidelines(canvas) { + + var ctx = canvas.getSelectionContext(), + //aligningLineOffset = 0, + //aligningLineMargin = 0, + //aligningLineWidth = 1, + //aligningLineColor = 'rgb(0,0,0)', + aligningLineOffset = 4, + aligningLineMargin = 5,//吸引 + aligningLineWidth = 2, + aligningLineColor = '#6637FF', + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + let width = canvas.getWidth(); + let height = canvas.getHeight(); + ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 + ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 + //修复中心放大后的位置偏移 2021-10-24 + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on('mouse:down', function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on('object:moving', function(e) { + + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // 清空辅助线数组,避免累积 + verticalLines.length = 0; + horizontalLines.length = 0; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + + if (canvasObjects[i] === activeObject) continue; + + // 跳过辅助线对象,避免辅助线之间相互对齐产生重影 + if (canvasObjects[i].isGuideLine) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + } + + // snap by the left edge + if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the right edge + if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + } + + // snap by the top edge + if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + } + + // snap by the bottom edge + if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on('before:render', function() { + + if(canvas&&canvas.contextTop) + { + canvas.clearContext(canvas.contextTop); + } + + + }); + + canvas.on('after:render', function() { + // 合并相同 x 坐标的竖向辅助线(使用四舍五入处理浮点数精度问题) + var mergedVerticalLines = {}; + for (var i = verticalLines.length; i--; ) { + var line = verticalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.x); + if (!mergedVerticalLines[key]) { + mergedVerticalLines[key] = { x: line.x, y1: line.y1, y2: line.y2 }; + } else { + // 合并 y 范围 + mergedVerticalLines[key].y1 = Math.min(mergedVerticalLines[key].y1, line.y1); + mergedVerticalLines[key].y2 = Math.max(mergedVerticalLines[key].y2, line.y2); + } + } + for (var key in mergedVerticalLines) { + drawVerticalLine(mergedVerticalLines[key]); + } + + // 合并相同 y 坐标的横向辅助线(使用四舍五入处理浮点数精度问题) + var mergedHorizontalLines = {}; + for (var i = horizontalLines.length; i--; ) { + var line = horizontalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.y); + if (!mergedHorizontalLines[key]) { + mergedHorizontalLines[key] = { y: line.y, x1: line.x1, x2: line.x2 }; + } else { + // 合并 x 范围 + mergedHorizontalLines[key].x1 = Math.min(mergedHorizontalLines[key].x1, line.x1); + mergedHorizontalLines[key].x2 = Math.max(mergedHorizontalLines[key].x2, line.x2); + } + } + for (var key in mergedHorizontalLines) { + drawHorizontalLine(mergedHorizontalLines[key]); + } + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on('mouse:up', function() { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + diff --git a/public/js/jr-qrcode.js b/frontend-electron/vendor/js/jr-qrcode.js similarity index 100% rename from public/js/jr-qrcode.js rename to frontend-electron/vendor/js/jr-qrcode.js diff --git a/public/js/jsencrypt.min.js b/frontend-electron/vendor/js/jsencrypt.min.js similarity index 100% rename from public/js/jsencrypt.min.js rename to frontend-electron/vendor/js/jsencrypt.min.js diff --git a/public/js/jspdf.umd.min.js b/frontend-electron/vendor/js/jspdf.umd.min.js similarity index 100% rename from public/js/jspdf.umd.min.js rename to frontend-electron/vendor/js/jspdf.umd.min.js diff --git a/public/js/print.min.js b/frontend-electron/vendor/js/print.min.js similarity index 100% rename from public/js/print.min.js rename to frontend-electron/vendor/js/print.min.js diff --git a/public/js/qrcode.min.js b/frontend-electron/vendor/js/qrcode.min.js similarity index 100% rename from public/js/qrcode.min.js rename to frontend-electron/vendor/js/qrcode.min.js diff --git a/layui/css/layui.css b/frontend-electron/vendor/layui/css/layui.css similarity index 100% rename from layui/css/layui.css rename to frontend-electron/vendor/layui/css/layui.css diff --git a/layui/css/modules/code.css b/frontend-electron/vendor/layui/css/modules/code.css similarity index 100% rename from layui/css/modules/code.css rename to frontend-electron/vendor/layui/css/modules/code.css diff --git a/layui/css/modules/laydate/default/font.css b/frontend-electron/vendor/layui/css/modules/laydate/default/font.css similarity index 100% rename from layui/css/modules/laydate/default/font.css rename to frontend-electron/vendor/layui/css/modules/laydate/default/font.css diff --git a/layui/css/modules/laydate/default/laydate.css b/frontend-electron/vendor/layui/css/modules/laydate/default/laydate.css similarity index 100% rename from layui/css/modules/laydate/default/laydate.css rename to frontend-electron/vendor/layui/css/modules/laydate/default/laydate.css diff --git a/layui/css/modules/layer/default/icon-ext.png b/frontend-electron/vendor/layui/css/modules/layer/default/icon-ext.png similarity index 100% rename from layui/css/modules/layer/default/icon-ext.png rename to frontend-electron/vendor/layui/css/modules/layer/default/icon-ext.png diff --git a/layui/css/modules/layer/default/icon.png b/frontend-electron/vendor/layui/css/modules/layer/default/icon.png similarity index 100% rename from layui/css/modules/layer/default/icon.png rename to frontend-electron/vendor/layui/css/modules/layer/default/icon.png diff --git a/layui/css/modules/layer/default/layer.css b/frontend-electron/vendor/layui/css/modules/layer/default/layer.css similarity index 100% rename from layui/css/modules/layer/default/layer.css rename to frontend-electron/vendor/layui/css/modules/layer/default/layer.css diff --git a/layui/css/modules/layer/default/loading-0.gif b/frontend-electron/vendor/layui/css/modules/layer/default/loading-0.gif similarity index 100% rename from layui/css/modules/layer/default/loading-0.gif rename to frontend-electron/vendor/layui/css/modules/layer/default/loading-0.gif diff --git a/layui/css/modules/layer/default/loading-1.gif b/frontend-electron/vendor/layui/css/modules/layer/default/loading-1.gif similarity index 100% rename from layui/css/modules/layer/default/loading-1.gif rename to frontend-electron/vendor/layui/css/modules/layer/default/loading-1.gif diff --git a/layui/css/modules/layer/default/loading-2.gif b/frontend-electron/vendor/layui/css/modules/layer/default/loading-2.gif similarity index 100% rename from layui/css/modules/layer/default/loading-2.gif rename to frontend-electron/vendor/layui/css/modules/layer/default/loading-2.gif diff --git a/layui/font/iconfont.eot b/frontend-electron/vendor/layui/font/iconfont.eot similarity index 100% rename from layui/font/iconfont.eot rename to frontend-electron/vendor/layui/font/iconfont.eot diff --git a/layui/font/iconfont.svg b/frontend-electron/vendor/layui/font/iconfont.svg similarity index 100% rename from layui/font/iconfont.svg rename to frontend-electron/vendor/layui/font/iconfont.svg diff --git a/layui/font/iconfont.ttf b/frontend-electron/vendor/layui/font/iconfont.ttf similarity index 100% rename from layui/font/iconfont.ttf rename to frontend-electron/vendor/layui/font/iconfont.ttf diff --git a/layui/font/iconfont.woff b/frontend-electron/vendor/layui/font/iconfont.woff similarity index 100% rename from layui/font/iconfont.woff rename to frontend-electron/vendor/layui/font/iconfont.woff diff --git a/layui/font/iconfont.woff2 b/frontend-electron/vendor/layui/font/iconfont.woff2 similarity index 100% rename from layui/font/iconfont.woff2 rename to frontend-electron/vendor/layui/font/iconfont.woff2 diff --git a/layui/layui.js b/frontend-electron/vendor/layui/layui.js similarity index 100% rename from layui/layui.js rename to frontend-electron/vendor/layui/layui.js diff --git a/layui/modules/all.js b/frontend-electron/vendor/layui/modules/all.js similarity index 100% rename from layui/modules/all.js rename to frontend-electron/vendor/layui/modules/all.js diff --git a/layui/modules/carousel.js b/frontend-electron/vendor/layui/modules/carousel.js similarity index 100% rename from layui/modules/carousel.js rename to frontend-electron/vendor/layui/modules/carousel.js diff --git a/layui/modules/code.js b/frontend-electron/vendor/layui/modules/code.js similarity index 100% rename from layui/modules/code.js rename to frontend-electron/vendor/layui/modules/code.js diff --git a/layui/modules/colorpicker.js b/frontend-electron/vendor/layui/modules/colorpicker.js similarity index 100% rename from layui/modules/colorpicker.js rename to frontend-electron/vendor/layui/modules/colorpicker.js diff --git a/layui/modules/demo.js b/frontend-electron/vendor/layui/modules/demo.js similarity index 100% rename from layui/modules/demo.js rename to frontend-electron/vendor/layui/modules/demo.js diff --git a/layui/modules/dropdown.js b/frontend-electron/vendor/layui/modules/dropdown.js similarity index 100% rename from layui/modules/dropdown.js rename to frontend-electron/vendor/layui/modules/dropdown.js diff --git a/layui/modules/element.js b/frontend-electron/vendor/layui/modules/element.js similarity index 100% rename from layui/modules/element.js rename to frontend-electron/vendor/layui/modules/element.js diff --git a/layui/modules/flow.js b/frontend-electron/vendor/layui/modules/flow.js similarity index 100% rename from layui/modules/flow.js rename to frontend-electron/vendor/layui/modules/flow.js diff --git a/layui/modules/form.js b/frontend-electron/vendor/layui/modules/form.js similarity index 100% rename from layui/modules/form.js rename to frontend-electron/vendor/layui/modules/form.js diff --git a/layui/modules/jquery.js b/frontend-electron/vendor/layui/modules/jquery.js similarity index 100% rename from layui/modules/jquery.js rename to frontend-electron/vendor/layui/modules/jquery.js diff --git a/layui/modules/lay.js b/frontend-electron/vendor/layui/modules/lay.js similarity index 100% rename from layui/modules/lay.js rename to frontend-electron/vendor/layui/modules/lay.js diff --git a/layui/modules/laydate.js b/frontend-electron/vendor/layui/modules/laydate.js similarity index 100% rename from layui/modules/laydate.js rename to frontend-electron/vendor/layui/modules/laydate.js diff --git a/layui/modules/layedit.js b/frontend-electron/vendor/layui/modules/layedit.js similarity index 100% rename from layui/modules/layedit.js rename to frontend-electron/vendor/layui/modules/layedit.js diff --git a/layui/modules/layer.js b/frontend-electron/vendor/layui/modules/layer.js similarity index 100% rename from layui/modules/layer.js rename to frontend-electron/vendor/layui/modules/layer.js diff --git a/layui/modules/laypage.js b/frontend-electron/vendor/layui/modules/laypage.js similarity index 100% rename from layui/modules/laypage.js rename to frontend-electron/vendor/layui/modules/laypage.js diff --git a/layui/modules/laytpl.js b/frontend-electron/vendor/layui/modules/laytpl.js similarity index 100% rename from layui/modules/laytpl.js rename to frontend-electron/vendor/layui/modules/laytpl.js diff --git a/layui/modules/layui.all.js b/frontend-electron/vendor/layui/modules/layui.all.js similarity index 100% rename from layui/modules/layui.all.js rename to frontend-electron/vendor/layui/modules/layui.all.js diff --git a/layui/modules/mobile.js b/frontend-electron/vendor/layui/modules/mobile.js similarity index 100% rename from layui/modules/mobile.js rename to frontend-electron/vendor/layui/modules/mobile.js diff --git a/layui/modules/rate.js b/frontend-electron/vendor/layui/modules/rate.js similarity index 100% rename from layui/modules/rate.js rename to frontend-electron/vendor/layui/modules/rate.js diff --git a/layui/modules/slider.js b/frontend-electron/vendor/layui/modules/slider.js similarity index 100% rename from layui/modules/slider.js rename to frontend-electron/vendor/layui/modules/slider.js diff --git a/layui/modules/table.js b/frontend-electron/vendor/layui/modules/table.js similarity index 100% rename from layui/modules/table.js rename to frontend-electron/vendor/layui/modules/table.js diff --git a/layui/modules/transfer.js b/frontend-electron/vendor/layui/modules/transfer.js similarity index 100% rename from layui/modules/transfer.js rename to frontend-electron/vendor/layui/modules/transfer.js diff --git a/layui/modules/tree.js b/frontend-electron/vendor/layui/modules/tree.js similarity index 100% rename from layui/modules/tree.js rename to frontend-electron/vendor/layui/modules/tree.js diff --git a/layui/modules/upload.js b/frontend-electron/vendor/layui/modules/upload.js similarity index 100% rename from layui/modules/upload.js rename to frontend-electron/vendor/layui/modules/upload.js diff --git a/layui/modules/util.js b/frontend-electron/vendor/layui/modules/util.js similarity index 100% rename from layui/modules/util.js rename to frontend-electron/vendor/layui/modules/util.js diff --git a/frontend-web/assets/css/auth.css b/frontend-web/assets/css/auth.css new file mode 100644 index 0000000..bb724f4 --- /dev/null +++ b/frontend-web/assets/css/auth.css @@ -0,0 +1,121 @@ +@import url('tokens.css'); +@import url('components.css'); + +.soon-auth-topbar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + height: 52px; +} + +body:has(.soon-auth-topbar) .soon-auth-page { + padding-top: 52px; + min-height: calc(100vh - 52px); +} + +.soon-auth-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 32px 20px 48px; + background: var(--soon-bg-content); + font-family: var(--soon-font); +} + +.soon-auth-shell { + width: 100%; + max-width: 420px; +} + +.soon-auth-brand { + text-align: center; + margin-bottom: 24px; +} + +.soon-auth-brand__logo { + width: 48px; + height: 48px; + margin-bottom: 12px; + border-radius: 10px; +} + +.soon-auth-brand h1 { + color: var(--soon-text-strong); + font-size: 22px; + font-weight: 600; + margin: 0 0 8px; + line-height: 1.3; +} + +.soon-auth-brand p { + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); + margin: 0; + line-height: 1.5; +} + +.soon-auth-form { + background: var(--soon-bg-panel); + border-radius: var(--soon-radius); + padding: 28px 24px; + border: 1px solid var(--soon-border-subtle); +} + +.soon-auth-form h2 { + color: var(--soon-text-strong); + font-size: 18px; + font-weight: 600; + margin: 0 0 6px; + text-align: center; +} + +.soon-auth-form__subtitle { + text-align: center; + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); + margin: 0 0 24px; + line-height: 1.5; +} + +.soon-auth-form .soon-auth-links { + text-align: center; + margin-top: var(--soon-space-lg); + font-size: var(--soon-font-sm); + color: var(--soon-text-muted); +} + +.soon-auth-form .soon-auth-links a { + color: var(--soon-accent); + text-decoration: none; + font-weight: 500; +} + +.soon-auth-form .soon-auth-links a:hover { text-decoration: underline; } + +.soon-pwd-toggle { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--soon-text-muted); + cursor: pointer; + font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} + +.soon-pwd-toggle:hover { + color: var(--soon-accent); + background: var(--soon-accent-soft); +} + +@media (max-width: 480px) { + .soon-auth-form { padding: 24px 18px; } + .soon-auth-brand h1 { font-size: 20px; } +} diff --git a/frontend-web/assets/css/components.css b/frontend-web/assets/css/components.css new file mode 100644 index 0000000..9a20bb7 --- /dev/null +++ b/frontend-web/assets/css/components.css @@ -0,0 +1,238 @@ +@import url('tokens.css'); + +.soon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--soon-space-sm); + padding: 0 20px; + height: 40px; + border: none; + border-radius: var(--soon-radius-sm); + background: var(--soon-accent); + color: #fff; + font-size: var(--soon-font-md); + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: background 0.2s var(--soon-ease), transform 0.15s var(--soon-ease); + box-sizing: border-box; +} +.soon-btn:hover { + background: var(--soon-accent-hover); +} +.soon-btn:active { transform: scale(0.98); } +.soon-btn--ghost { + background: transparent; + border: 1px solid var(--soon-border); + color: var(--soon-text); +} +.soon-btn--ghost:hover { border-color: var(--soon-accent); color: var(--soon-accent); background: transparent; } +.soon-btn--sm { + height: var(--soon-btn-h-sm); + padding: 0 14px; + font-size: var(--soon-font-sm); +} +.soon-btn--block { width: 100%; } + +.soon-input--sm { + height: var(--soon-input-h-sm); + padding: 0 12px; + font-size: var(--soon-font-sm); +} + +.soon-section-title { + display: flex; + align-items: center; + gap: var(--soon-space-sm); + margin: 0 0 var(--soon-space-md); + color: var(--soon-text-strong); + font-size: var(--soon-font-lg); + font-weight: 600; +} +.soon-section-title__badge { margin-left: 0; } + +.soon-portal-topbar__link { + color: var(--soon-text); + text-decoration: none; + font-size: var(--soon-font-sm); +} +.soon-portal-topbar__link:hover { color: var(--soon-accent); text-decoration: none; } +.soon-portal-topbar__link--muted { color: var(--soon-text-muted); } + +.soon-form-label { + display: block; + margin: 0 0 var(--soon-space-sm); + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); +} +.soon-form-field { margin-bottom: var(--soon-space-md); } + +.soon-auth-error-box { + display: none; + margin-top: var(--soon-space-sm); + margin-bottom: var(--soon-space-md); + padding: var(--soon-space-sm) var(--soon-space-md); + background: rgba(229, 115, 115, 0.12); + border: 1px solid rgba(229, 115, 115, 0.35); + border-radius: var(--soon-radius-sm); + color: var(--soon-danger); + font-size: var(--soon-font-sm); +} + +.soon-card--lost .rect { border: 1px dashed var(--soon-danger) !important; } + +.soon-pay-banner { + position: fixed; + top: 52px; + left: 0; + right: 0; + z-index: 999; + padding: var(--soon-space-sm) var(--soon-space-lg); + background: rgba(0, 150, 136, 0.92); + color: #fff; + text-align: center; + font-size: var(--soon-font-sm); + animation: soon-fadeInUp 0.3s var(--soon-ease); +} + +.soon-dialog-body { + padding: var(--soon-space-lg); + color: var(--soon-text); + font-size: var(--soon-font-md); + text-align: left; +} + +.soon-skeleton--h80 { height: 80px; } +.soon-skeleton--h120 { height: 120px; } +.soon-skeleton--h200 { height: 200px; width: 200px; } + +.soon-card { + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius); + padding: var(--soon-space-lg); + transition: transform 0.2s var(--soon-ease), border-color 0.2s var(--soon-ease); +} +.soon-card:hover { + transform: translateY(-2px); + border-color: var(--soon-accent); +} + +.soon-input-wrap { + position: relative; + margin-bottom: var(--soon-space-md); +} +.soon-input-wrap .soon-input-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + opacity: 0.5; + pointer-events: none; +} +.soon-input-wrap .soon-input-icon svg { + display: block; + width: 18px; + height: 18px; + stroke: var(--soon-text-muted); +} +.soon-input { + width: 100%; + height: 44px; + padding: 0 40px 0 40px; + background: var(--soon-bg-deep); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius-sm); + color: var(--soon-text-strong); + font-size: var(--soon-font-md); + outline: none; + transition: border-color 0.2s; + box-sizing: border-box; +} +.soon-input:focus { + border-color: var(--soon-accent); + box-shadow: var(--soon-focus-ring); +} +.soon-input.is-error { border-color: var(--soon-danger); animation: soon-shake 0.3s; } + +.soon-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: var(--soon-font-xs); + font-weight: 500; +} +.soon-badge--active { background: rgba(0, 150, 136, 0.2); color: var(--soon-accent); } +.soon-badge--disabled { background: rgba(229, 115, 115, 0.2); color: var(--soon-danger); } +.soon-badge--pending { background: rgba(255, 183, 77, 0.2); color: var(--soon-warning); } +.soon-badge--paid { background: rgba(102, 187, 106, 0.2); color: var(--soon-success); } + +.soon-skeleton { + background: linear-gradient(90deg, #2a3036 25%, #3a424a 50%, #2a3036 75%); + background-size: 200% 100%; + animation: soon-shimmer 1.2s infinite; + border-radius: var(--soon-radius-sm); +} + +.soon-empty { + text-align: center; + padding: var(--soon-space-lg) var(--soon-space-md); + color: var(--soon-text-muted); +} +.soon-empty--inline { + grid-column: 1 / -1; + min-height: 200px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 28px 16px 20px; +} +.soon-home-panel .soon-empty--inline { + min-height: 160px; +} +.soon-empty__icon { + width: 160px; + height: 160px; + margin-bottom: 16px; + display: block; + flex-shrink: 0; + object-fit: contain; + pointer-events: none; + user-select: none; + background: transparent; +} +.soon-empty img.soon-empty__icon { + width: 160px; + height: 160px; +} +.soon-empty__title { + margin: 0; + font-size: var(--soon-font-sm); + font-weight: 400; + color: var(--soon-text-muted); + line-height: 1.5; +} +.soon-empty__hint { + margin: 6px 0 0 !important; + font-size: var(--soon-font-xs) !important; + color: var(--soon-text-muted) !important; + opacity: 0.75; + line-height: 1.5; + max-width: 320px; +} +.soon-empty__action { + margin-top: var(--soon-space-md); +} +.soon-empty p { + margin: 0; + font-size: var(--soon-font-md); + color: var(--soon-text-strong); +} + +.soon-toast-error { color: var(--soon-danger); font-size: var(--soon-font-sm); margin-top: var(--soon-space-sm); } diff --git a/frontend-web/assets/css/design.css b/frontend-web/assets/css/design.css new file mode 100644 index 0000000..d815487 --- /dev/null +++ b/frontend-web/assets/css/design.css @@ -0,0 +1,715 @@ + +html,body{ + background-color: #31373D; + color: #ffffff; + user-select: none; + height: 100%; + overflow: hidden; +} +img { + -webkit-user-drag: none; +} +.main{ + padding: 0 !important; + height: 100%; +} +.col-left{ + margin-right: 320px; +} +.col-right{ + float: right; + height: 100%; + width: 320px; +} +.tools-bar{ + height: 50px; + width: 100%; + border-bottom: 1px solid #000000; +} +.left-bar{ + border-right: 1px solid #000000; +} +.left-bar .tool{ + margin-left: 10px; + display: inline-block; + width: 32px; + height: 32px; + margin-top: 9px; +} +.left-bar .tool:hover{ + background-color: #646E76; +} +.left-bar .tool img{ + height: 24px; + width: 24px; + margin-top: 4px; + margin-left: 4px; +} +.right-bar .tool{ + margin-left: 14px; + display: inline-block; + width: 32px; + height: 32px; + margin-top: 9px; +} +.right-bar .tool:hover{ + background-color: #646E76; +} +.right-bar .tool img{ + height: 24px; + width: 24px; + margin-top: 4px; + margin-left: 4px; +} +.mode1{ + display: inline-block; + border-right: 1px solid #000000; + padding-right: 10px; + height: 50px; +} +.mode2{ + + display: inline-block; + height: 50px; + padding-right: 10px; + border-right: 1px solid #000000; +} +.mode3{ + float: right; + line-height: 50px; + margin-right: 30px; +} +.white{ + color: white; +} +.white:hover{ + color: white !important; +} + +.container{ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.container .left{ + position: absolute; + top: 50px; + bottom: 0px; + width: 50px; + border-right: 1px solid #000000; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; +} + +.container .right{ + margin-left: 50px; + border-right: 1px solid #000000; + height: 100%; +} +#canvas-div{ + position: relative; +} + +.canvas_box { + position: relative; + z-index: 4; + display: flex; + align-items: center; + justify-content: start; +} +.canvas_bg { + position: absolute; + top: 0; + left: 0; + z-index: 1; + opacity: 0; + width: 100%; + height: 100%; +} +.canvas_bg_img { + max-width: calc(100% - 470px); + max-height: calc(100% - 180px); +} +.container .tool{ + width: 32px; + height: 32px; + margin-top: 15px; + display: flex; + justify-content: center; + align-items: center; +} +.container .tool img{ + width: 24px; + height: 24px; +} +.container .tool:hover{ + background-color: #646E76; +} + +.layui-form-label{ + width: 45px; + padding-left: 5px; +} + +.layui-input-block { + margin-left: 70px; + +} + +.component-title{ + height: 32px; + line-height: 32px; + width: 100%; + font-size: 18px; + background-color: #4E575F; + text-align: center; + padding-left: 10px; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; +} +.component-line{ + font-size: 14px; + padding: 5px; +} +.component-line span{ + color: #969FA7; +} +.component-line .tool { + cursor: pointer; + padding: 4px; +} +.component-line .tool:hover{ + background-color: #646E76; +} +.component-line .tool1 { + background-color: #646E76; + padding: 4px; +} +.ui-input{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; +} +.ui-input:hover{ + background-color: #646E76; +} +textarea{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; +} +textarea:hover{ + background-color: #646E76; +} +.control{ + display: none; +} +#base_control{ + display: none; +} +.pic_bg{ + display: inline-block; + background-color: #31373D; + border: #646E76 solid 1px; + padding: 3px 10px; + color: #ffffff; + font-size: 16px; + position: relative; + margin-left: 5px; + cursor: pointer; + border-radius: 6px; +} +.pic_bg:hover{ + background-color: #646E76; +} + + +.input_color{ + position: relative; + top:5px; +} +.input_color[type='color']::-webkit-color-swatch-wrapper{padding: 0;} +.input_color[type='color']::-webkit-color-swatch{border: 0;} + +.radio{ + position: relative; + top:2px; + left:3px +} + +.ui-select{ + background-color: #31373D; + border: #646E76 solid 1px; + color: #ffffff; + font-size: 16px; +} + + +::-webkit-scrollbar +{ + width: 8px; + height: 8px; + background-color: #31373D; +} + +/*定义滚动条轨道 内阴影+圆角*/ +::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px; + background-color: #F5F5F5; +} + +/*定义滑块 内阴影+圆角*/ +::-webkit-scrollbar-thumb +{ + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; +} + +.ui-button{ + display: inline-block; + background-color: #31373D; + border: #646E76 solid 1px; + padding: 3px 10px; + color: #ffffff; + font-size: 16px; + position: relative; + margin-left: 5px; + cursor: pointer; + border-radius: 6px; +} +.ui-button:hover{ + background-color: #646E76; +} + +.ui-button-active{ + background-color: #646E76; +} +.ui-radio{ + +} +.ui-radio-acitve{ + background-color: #646E76; +} +.ui-radio:hover{ + background-color: #646E76; +} +.obj_list{ + overflow-y: auto; + +} +.obj_list div{ + height: 32px; + line-height: 32px; + width: 100%; + font-size: 14px; + padding-left: 10px; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + overflow: hidden; +} +.obj_list img{ + position: relative; + right: 4px; + width: 24px; +} +.obj_list div:hover{ + background-color: #6F7A84; +} + +#posText{ + line-height: 50px; + font-size: 18px; + margin-left: 10px; +} + +#rotate:hover{ + background-color: #646E76; +} + +#object_attribute{ + height: 470px; + overflow-y: auto; +} + +.transparent-button{ + display: inline-block; + height: 19px; + border: #646E76 solid 1px; + position: relative; + bottom: 2px; + padding: 3px; +} +.transparent-button:hover{ + background-color: #646E76; +} + + +.mode-left{ + display: inline-block; +} +.mode-warp{ + display: flex; + height: 50px; + align-items: center; +} +.mode-left .img-warp{ + height: 32px; + width: 32px; + display: flex; + justify-content: center; + align-items: center; + margin-left: 10px; +} + +.mode-left .img-warp:hover{ + background-color: #646E76; +} + +.mode-left .img-warp1 { + width: 0; + height: 0; + overflow: hidden; + margin: 0; + padding: 0; + opacity: 0; + border: none; +} + + +.line{ + height: 50px; + width: 1px; + background-color: #000; + margin-left: 10px; +} + +.rotate-div{ + position: relative; + transition: 200ms; +} +.rotate-wrap{ + position: absolute; + top:0px; + left:0px; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,.3); + visibility: hidden; + border-radius: 3%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.rotate-div:hover .rotate-wrap{ + visibility: visible; +} + +/* ── Web 设计页:design1 / design2 统一视口布局(无页面滚动) ── */ +html:has(body.soon-design-page) { + height: 100%; + overflow: hidden; +} + +body.soon-design-page { + height: 100vh; + max-height: 100vh; + overflow: hidden; + margin: 0; + padding: 0 !important; + display: flex; + flex-direction: column; + box-sizing: border-box; + background-color: #1e1e1e; + -webkit-font-smoothing: antialiased; +} + +body.soon-design-page .soon-portal-topbar { + position: static; + width: 100%; + flex-shrink: 0; +} + +body.soon-design-page #portal-topbar { + flex-shrink: 0; +} + +body.soon-design-page .layui-fluid.main { + flex: 1; + min-height: 0; + height: auto !important; + overflow: hidden; + padding: 0 !important; + display: flex; + flex-direction: column; +} + +body.soon-design-page .layui-row { + flex: 1; + min-height: 0; + display: flex; + flex-direction: row-reverse; + height: 100%; + width: 100%; + margin: 0; + overflow: hidden; +} + +body.soon-design-page .col-left { + flex: 1; + min-width: 0; + margin-right: 0; + float: none; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + border-right: 1px solid #1e1e1e; +} + +body.soon-design-page .col-right { + flex-shrink: 0; + width: 320px; + float: none; + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #31373d; +} + +body.soon-design-page .col-right > .tools-bar { + flex-shrink: 0; +} + +body.soon-design-page .tools-bar { + flex-shrink: 0; + background-color: #31373d; +} + +body.soon-design-page #version_change { + white-space: nowrap; + flex-shrink: 0; + min-width: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +body.soon-design-page .img-warp { + flex-shrink: 0; +} + +body.soon-design-page .container { + flex: 1; + display: flex; + min-height: 0; + height: 0; + overflow: hidden; + position: relative; + box-sizing: border-box; +} + +body.soon-design-page .container .left { + position: static; + top: auto; + bottom: auto; + width: 50px; + flex-shrink: 0; + height: auto; + align-self: stretch; + overflow-y: auto; + overflow-x: hidden; + background-color: #31373d; + border-right: 1px solid #000; + -ms-overflow-style: none; + scrollbar-width: none; +} + +body.soon-design-page .container .left::-webkit-scrollbar { + display: none; +} + +body.soon-design-page .container .right { + flex: 1; + min-width: 0; + min-height: 0; + margin-left: 0; + height: auto; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #1e252d; + border-right: none; +} + +body.soon-design-page #canvas-div { + flex: 1; + min-height: 0; + position: relative; + overflow: hidden; + width: 100%; + display: flex; +} + +body.soon-design-page .canvas-wrapper { + width: 100%; + height: 100%; + overflow: auto; + position: relative; +} + +body.soon-design-page .footer { + height: 30px; + flex-shrink: 0; + display: flex; + align-items: center; + padding: 0 0 0 10px; + box-sizing: border-box; + background-color: #31373d; + color: #ccc; + border-top: 1px solid #1e1e1e; + z-index: 100; +} + +body.soon-design-page #posText { + line-height: 30px; + font-size: 14px; + margin-left: 0; +} + +body.soon-design-page #object_attribute { + flex: 1 1 auto; + min-height: 0; + height: auto !important; + overflow-y: auto; + overflow-x: hidden; +} + +body.soon-design-page #obj_list { + flex: 0 0 auto; + max-height: 38%; + min-height: 120px; + display: flex; + flex-direction: column; + overflow: hidden; + border-top: 1px solid #1e1e1e; +} + +body.soon-design-page #obj_list .component-title { + flex-shrink: 0; +} + +body.soon-design-page .obj_list { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + background-color: #31373d; +} + +body.soon-design-page .layer-row { + display: flex !important; + justify-content: space-between; + align-items: center; + height: 36px; + padding: 0 5px; + cursor: pointer; + border-bottom: 1px solid #444; + background-color: transparent; + color: #ccc; + font-size: 12px; + position: relative; +} + +body.soon-design-page .layer-row:hover { + background-color: #4a525a; +} + +body.soon-design-page .layer-left { + display: flex; + align-items: center; + overflow: hidden; + white-space: nowrap; + max-width: 180px; +} + +body.soon-design-page .layer-index { + min-width: 24px; + text-align: center; + color: #999; + border-left: 1px solid #444; + padding-left: 5px; +} + +body.soon-design-page .ruler-container { + position: relative; + background-color: #2c2c2c; + border: 1px solid #1e1e1e; + overflow: hidden; +} + +body.soon-design-page .ruler-horizontal { + position: absolute; + top: 0; + left: 0; + height: 20px; + background-color: rgba(44, 44, 44, 0.9) !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + z-index: 1000; + cursor: row-resize; +} + +body.soon-design-page .ruler-vertical { + position: absolute; + top: 0; + left: 0; + width: 20px; + background-color: rgba(44, 44, 44, 0.9) !important; + border-right: 1px solid rgba(255, 255, 255, 0.2); + z-index: 1000; + cursor: col-resize; +} + +body.soon-design-page .ruler-corner { + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background-color: rgba(44, 44, 44, 0.9) !important; + border-right: 1px solid rgba(255, 255, 255, 0.2); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + z-index: 1001; +} + +body.soon-design-page .guide-line-drag { + position: absolute; + background-color: #0066ff; + z-index: 9999; + pointer-events: none; + box-shadow: 0 0 6px #0066ff; + opacity: 0.9; +} + +body.soon-design-page #source_front, +body.soon-design-page #source_front2, +body.soon-design-page #source_back { + width: 2787px; + height: 2787px; + border-radius: 50%; +} \ No newline at end of file diff --git a/frontend-web/assets/css/index.css b/frontend-web/assets/css/index.css new file mode 100644 index 0000000..5d85dd1 --- /dev/null +++ b/frontend-web/assets/css/index.css @@ -0,0 +1 @@ +@import url('portal.css'); diff --git a/frontend-web/assets/css/layer-soon.css b/frontend-web/assets/css/layer-soon.css new file mode 100644 index 0000000..639aa01 --- /dev/null +++ b/frontend-web/assets/css/layer-soon.css @@ -0,0 +1,209 @@ +@import url('tokens.css'); + +.layui-layer.soon-layer, +.layui-layer-soon-layer { + background-color: var(--soon-bg-panel) !important; + border: 1px solid var(--soon-border-subtle) !important; + box-shadow: var(--soon-shadow) !important; + color: var(--soon-text) !important; + border-radius: var(--soon-radius) !important; +} +.layui-layer.soon-layer .layui-layer-title, +.layui-layer-soon-layer .layui-layer-title { + background-color: var(--soon-bg-elevated) !important; + border-bottom: 1px solid var(--soon-border-subtle) !important; + color: var(--soon-text-strong) !important; +} +.layui-layer.soon-layer .layui-layer-content, +.layui-layer-soon-layer .layui-layer-content { + background-color: var(--soon-bg-panel) !important; + color: var(--soon-text) !important; +} + +.layui-layer.soon-layer.soon-layer--pay .layui-layer-content, +.layui-layer-soon-layer.soon-layer--pay .layui-layer-content { + overflow: visible !important; + max-height: none !important; +} + +.layui-layer.soon-layer.soon-layer--pay, +.layui-layer-soon-layer.soon-layer--pay { + max-height: calc(100vh - 32px); +} + +.layui-layer.soon-layer.soon-layer--subscribe, +.layui-layer-soon-layer.soon-layer--subscribe { + width: min(1000px, calc(100vw - 24px)) !important; + max-height: calc(100vh - 24px) !important; +} + +.layui-layer.soon-layer.soon-layer--subscribe .layui-layer-content, +.layui-layer-soon-layer.soon-layer--subscribe .layui-layer-content { + overflow: visible !important; + height: auto !important; + max-height: calc(100vh - 24px) !important; + padding: 0 !important; + box-sizing: border-box !important; +} + +.layui-layer.soon-layer.soon-layer--subscribe .layui-layer-setwin, +.layui-layer-soon-layer.soon-layer--subscribe .layui-layer-setwin { + top: 14px; + right: 14px; +} + +.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll, +.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll { + overflow-x: hidden !important; + overflow-y: auto !important; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.22) transparent; +} + +.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar, +.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar-track, +.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar-track { + background: transparent; +} + +.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar-thumb, +.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); + border-radius: 4px; + border: 2px solid transparent; + background-clip: padding-box; +} + +.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar-thumb:hover, +.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.28); + background-clip: padding-box; +} +.layui-layer.soon-layer .layui-layer-btn, +.layui-layer-soon-layer .layui-layer-btn { + background-color: var(--soon-bg-panel) !important; + border-top: 1px solid var(--soon-border-subtle) !important; +} +.layui-layer.soon-layer .layui-layer-btn a, +.layui-layer-soon-layer .layui-layer-btn a { + background-color: var(--soon-bg-elevated) !important; + border-color: var(--soon-border-subtle) !important; + color: var(--soon-text) !important; + border-radius: var(--soon-radius-sm) !important; +} +.layui-layer.soon-layer .layui-layer-btn .layui-layer-btn0, +.layui-layer-soon-layer .layui-layer-btn .layui-layer-btn0 { + background-color: var(--soon-accent) !important; + border-color: var(--soon-accent) !important; + color: #fff !important; +} + +.layui-layer-dialog .layui-layer-content { + background-color: var(--soon-bg-panel); + color: var(--soon-text); +} +.layui-layer-dialog .layui-layer-title { + background-color: var(--soon-bg-elevated); + color: var(--soon-text-strong); + border-bottom: 1px solid var(--soon-border-subtle); +} + +.layui-layer.soon-toast, +.layui-layer.soon-layer-msg, +.layui-layer.layui-layer-msg { + min-width: 0 !important; + max-width: min(420px, calc(100vw - 32px)); + background: rgba(54, 61, 69, 0.96) !important; + border: 1px solid var(--soon-border-subtle) !important; + border-radius: var(--soon-radius-lg) !important; + box-shadow: var(--soon-shadow), 0 0 0 1px rgba(0, 0, 0, 0.25) !important; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + animation: soon-toast-in 0.28s var(--soon-ease) both; + overflow: hidden; +} + +.layui-layer.soon-toast::before, +.layui-layer.soon-layer-msg::before, +.layui-layer.layui-layer-msg::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--soon-accent); + border-radius: var(--soon-radius-lg) 0 0 var(--soon-radius-lg); +} + +.layui-layer.soon-toast.soon-toast--success::before, +.layui-layer.soon-toast--success::before { background: var(--soon-success); } +.layui-layer.soon-toast.soon-toast--error::before, +.layui-layer.soon-toast--error::before { background: var(--soon-danger); } +.layui-layer.soon-toast.soon-toast--warn::before, +.layui-layer.soon-toast--warn::before { background: var(--soon-warning); } + +.layui-layer.soon-toast .layui-layer-content, +.layui-layer.soon-layer-msg .layui-layer-content, +.layui-layer.layui-layer-msg .layui-layer-content { + padding: 14px 20px 14px 22px !important; + line-height: 1.5 !important; + font-size: var(--soon-font-md) !important; + color: var(--soon-text-strong) !important; + background: transparent !important; + text-align: left; + word-break: break-word; +} + +.layui-layer.soon-toast .layui-layer-ico, +.layui-layer.soon-layer-msg .layui-layer-ico, +.layui-layer.layui-layer-msg .layui-layer-ico { + display: none !important; +} + +.layui-layer.soon-toast.layui-layer-padding .layui-layer-content, +.layui-layer.soon-layer-msg.layui-layer-padding .layui-layer-content { + padding-left: 22px !important; +} + +@keyframes soon-toast-in { + from { + opacity: 0; + transform: translateY(-10px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.layui-layer-prompt .layui-layer-input { + background: var(--soon-bg-deep) !important; + border: 1px solid var(--soon-border-subtle) !important; + color: var(--soon-text-strong) !important; + border-radius: var(--soon-radius-sm) !important; +} + +.soon-design-portal-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 60px; + z-index: 10000; + flex-shrink: 0; +} + +body.soon-design-page { + padding-top: 60px !important; + box-sizing: border-box; +} + +body.soon-design-page .layui-fluid { + height: calc(100% - 60px) !important; +} diff --git a/frontend-web/assets/css/member.css b/frontend-web/assets/css/member.css new file mode 100644 index 0000000..778febf --- /dev/null +++ b/frontend-web/assets/css/member.css @@ -0,0 +1,1238 @@ +@import url('tokens.css'); + +/* ── 页面基底 ── */ +.soon-member-page { + min-height: 100vh; + background-color: #1e2228; + background-image: + radial-gradient(ellipse 90% 60% at 10% -10%, rgba(0, 150, 136, 0.18), transparent 55%), + radial-gradient(ellipse 70% 50% at 95% 5%, rgba(38, 166, 154, 0.1), transparent 50%), + radial-gradient(ellipse 50% 40% at 50% 100%, rgba(0, 0, 0, 0.35), transparent), + linear-gradient(180deg, #252a31 0%, #1e2228 40%, #1a1e24 100%); + font-family: var(--soon-font); + padding: calc(52px + 36px) clamp(20px, 4vw, 40px) 56px; + color: var(--soon-text); +} + +.soon-member-wrap { + max-width: 1140px; + margin: 0 auto; +} + +.soon-portal-topbar a.soon-portal-topbar__link.is-active { + color: var(--soon-accent); +} + +/* ── 页头 ── */ +.soon-member-pagehead { + margin-bottom: 28px; + animation: soon-fadeInUp 0.45s var(--soon-ease); +} + +.soon-member-pagehead__eyebrow { + margin: 0 0 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--soon-accent); +} + +.soon-member-pagehead__title { + margin: 0 0 8px; + font-size: clamp(28px, 4vw, 34px); + font-weight: 700; + color: var(--soon-text-strong); + letter-spacing: -0.02em; +} + +.soon-member-pagehead__sub { + margin: 0; + font-size: 15px; + color: var(--soon-text-muted); + line-height: 1.5; +} + +/* ── 当前会员仪表盘 ── */ +.soon-member-dashboard { + margin-bottom: 48px; + animation: soon-fadeInUp 0.5s var(--soon-ease) 0.05s backwards; +} + +.soon-member-dash { + position: relative; + border-radius: 16px; + overflow: hidden; + background: linear-gradient(145deg, rgba(54, 61, 69, 0.95) 0%, rgba(43, 48, 54, 0.98) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.06) inset, + 0 20px 50px rgba(0, 0, 0, 0.35); +} + +.soon-member-dash::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient(180deg, #26a69a, var(--soon-accent)); +} + +.soon-member-dash__body { + padding: 28px 32px 0; +} + +.soon-member-dash__body--guest { + padding-bottom: 28px; +} + +.soon-member-dash__body--guest .soon-member-dash__desc { + margin-top: 8px; +} + +.soon-member-dash__can { + margin: 16px 0 0; + padding: 0; + list-style: none; +} + +.soon-member-dash__can li { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 14px; + color: var(--soon-text); + line-height: 1.5; +} + +.soon-member-dash__hint { + margin: 14px 0 0; + padding: 12px 14px; + border-radius: 8px; + font-size: 13px; + color: var(--soon-text-muted); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); + line-height: 1.55; +} + +.soon-member-dash__actions { + margin-top: 18px; +} + +.soon-member-dash__top { + display: grid; + grid-template-columns: 1fr auto; + gap: 24px; + align-items: start; +} + +@media (max-width: 720px) { + .soon-member-dash__top { + grid-template-columns: 1fr; + } +} + +.soon-member-dash__badge-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 14px; +} + +.soon-member-tier { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + background: rgba(0, 150, 136, 0.15); + color: #4db6ac; + border: 1px solid rgba(0, 150, 136, 0.25); +} + +.soon-member-tier--free { + background: rgba(255, 255, 255, 0.06); + color: var(--soon-text-muted); + border-color: rgba(255, 255, 255, 0.1); +} + +.soon-member-status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; +} + +.soon-member-status-pill::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.soon-member-status-pill--free { + background: rgba(255, 255, 255, 0.05); + color: var(--soon-text-muted); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.soon-member-status-pill--active { + background: rgba(0, 150, 136, 0.12); + color: #4db6ac; + border: 1px solid rgba(0, 150, 136, 0.3); +} + +.soon-member-status-pill--expiring { + background: rgba(255, 183, 77, 0.12); + color: var(--soon-warning); + border: 1px solid rgba(255, 183, 77, 0.35); +} + +.soon-member-dash__title { + margin: 0 0 8px; + font-size: 24px; + font-weight: 700; + color: var(--soon-text-strong); + letter-spacing: -0.01em; +} + +.soon-member-dash__desc { + margin: 0; + font-size: 14px; + color: var(--soon-text-muted); + line-height: 1.65; + max-width: 520px; +} + +.soon-member-dash__expire { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 12px; + font-size: 13px; + color: var(--soon-text); +} + +.soon-member-dash__expire strong { + color: var(--soon-text-strong); + font-weight: 600; +} + +.soon-member-storage-ring { + width: 120px; + height: 120px; + position: relative; + flex-shrink: 0; +} + +.soon-member-storage-ring svg { + transform: rotate(-90deg); + width: 100%; + height: 100%; +} + +.soon-member-storage-ring__track { + fill: none; + stroke: rgba(255, 255, 255, 0.06); + stroke-width: 8; +} + +.soon-member-storage-ring__fill { + fill: none; + stroke: url(#memberRingGrad); + stroke-width: 8; + stroke-linecap: round; + transition: stroke-dashoffset 0.6s var(--soon-ease); +} + +.soon-member-storage-ring__fill--warn { + stroke: url(#memberRingWarn); +} + +.soon-member-storage-ring__center { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.soon-member-storage-ring__pct { + font-size: 22px; + font-weight: 700; + color: var(--soon-text-strong); + line-height: 1; +} + +.soon-member-storage-ring__label { + margin-top: 4px; + font-size: 11px; + color: var(--soon-text-muted); +} + +.soon-member-dash__usage { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.soon-member-dash__usage-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + font-size: 13px; +} + +.soon-member-dash__usage-head span { + color: var(--soon-text-muted); +} + +.soon-member-dash__usage-head strong { + color: var(--soon-text-strong); + font-weight: 600; + font-size: 14px; +} + +.soon-member-progress { + height: 10px; + background: rgba(0, 0, 0, 0.35); + border-radius: 5px; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; +} + +.soon-member-progress__bar { + height: 100%; + min-width: 0; + background: linear-gradient(90deg, #00897b, #26a69a, #4db6ac); + border-radius: 5px; + transition: width 0.5s var(--soon-ease); + box-shadow: 0 0 12px rgba(0, 150, 136, 0.4); +} + +.soon-member-progress__bar--warn { + background: linear-gradient(90deg, #f57c00, var(--soon-warning)); + box-shadow: 0 0 12px rgba(255, 183, 77, 0.35); +} + +.soon-member-dash__stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(0, 0, 0, 0.15); +} + +@media (max-width: 720px) { + .soon-member-dash__stats { + grid-template-columns: repeat(2, 1fr); + } +} + +.soon-member-dash__stat { + padding: 18px 24px; + text-align: center; + border-right: 1px solid rgba(255, 255, 255, 0.05); +} + +.soon-member-dash__stat:last-child { + border-right: none; +} + +@media (max-width: 720px) { + .soon-member-dash__stat:nth-child(2) { + border-right: none; + } + .soon-member-dash__stat { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + .soon-member-dash__stat:nth-last-child(-n+2) { + border-bottom: none; + } +} + +.soon-member-dash__stat-value { + font-size: 20px; + font-weight: 700; + color: var(--soon-text-strong); + line-height: 1.2; +} + +.soon-member-dash__stat-label { + margin-top: 4px; + font-size: 12px; + color: var(--soon-text-muted); +} + +/* ── 区块标题 ── */ +.soon-member-section { + margin-bottom: 44px; + animation: soon-fadeInUp 0.5s var(--soon-ease) backwards; +} + +.soon-member-section:nth-of-type(2) { animation-delay: 0.1s; } +.soon-member-section:nth-of-type(3) { animation-delay: 0.15s; } + +.soon-member-section--last { + margin-bottom: 0; +} + +.soon-member-section__head { + margin-bottom: 22px; +} + +.soon-member-section__head h2 { + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 6px; + font-size: 18px; + font-weight: 600; + color: var(--soon-text-strong); +} + +.soon-member-section__mark { + display: inline-block; + width: 4px; + height: 18px; + border-radius: 2px; + background: linear-gradient(180deg, var(--soon-accent), #26a69a); +} + +.soon-member-section__head p { + margin: 6px 0 0; + padding-left: 14px; + color: var(--soon-text-muted); + font-size: 14px; + line-height: 1.6; + max-width: 720px; +} + +/* ── 套餐卡片 ── */ +.soon-plans-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; + align-items: stretch; +} + +@media (min-width: 961px) { + .soon-plans-grid { + align-items: stretch; + } +} + +@media (max-width: 960px) { + .soon-plans-grid { + grid-template-columns: 1fr; + max-width: 420px; + margin: 0 auto; + } +} + +.soon-plan-card { + position: relative; + display: flex; + flex-direction: column; + padding: 0; + border-radius: 14px; + overflow: hidden; + background: rgba(54, 61, 69, 0.6); + border: 1px solid rgba(255, 255, 255, 0.07); + backdrop-filter: blur(8px); + transition: transform 0.25s var(--soon-ease), box-shadow 0.25s, border-color 0.25s; + animation: soon-fadeInUp 0.45s var(--soon-ease) backwards; +} + +.soon-plan-card:nth-child(1) { animation-delay: 0.12s; } +.soon-plan-card:nth-child(2) { animation-delay: 0.18s; } +.soon-plan-card:nth-child(3) { animation-delay: 0.24s; } + +.soon-plan-card:hover { + transform: translateY(-4px); + border-color: rgba(0, 150, 136, 0.35); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.3); +} + +.soon-plan-card--featured { + border-color: rgba(0, 150, 136, 0.5); + background: linear-gradient(165deg, rgba(0, 150, 136, 0.12) 0%, rgba(54, 61, 69, 0.85) 45%); + box-shadow: + 0 0 0 1px rgba(0, 150, 136, 0.25), + 0 12px 36px rgba(0, 150, 136, 0.12); +} + +.soon-plan-card--featured:hover { + transform: translateY(-4px); +} + +.soon-plan-card--current { + border-color: rgba(255, 255, 255, 0.12); +} + +.soon-plan-card__ribbon { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 7px 14px 7px 18px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + line-height: 1.2; + white-space: nowrap; + border-bottom-left-radius: 12px; +} + +.soon-plan-card__ribbon--rec { + background: linear-gradient(135deg, var(--soon-accent), #26a69a); + color: #fff; + box-shadow: -2px 2px 8px rgba(0, 150, 136, 0.3); +} + +.soon-plan-card__ribbon--current { + background: rgba(255, 255, 255, 0.08); + color: var(--soon-text-muted); +} + +.soon-plan-card__tier { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--soon-accent); + margin-bottom: 8px; +} + +.soon-plan-card--current .soon-plan-card__tier { + color: var(--soon-text-muted); +} + +.soon-plan-card__name { + font-size: 18px; + font-weight: 700; + color: var(--soon-text-strong); + margin-bottom: 6px; + line-height: 1.3; +} + +.soon-plan-card__desc { + margin: 0; + font-size: 13px; + color: var(--soon-text-muted); + line-height: 1.55; + min-height: 0; + word-break: normal; +} + +.soon-plan-card__save { + position: absolute; + top: 12px; + left: 12px; + z-index: 2; + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + line-height: 1.2; + white-space: nowrap; + color: #ffe082; + background: rgba(255, 193, 7, 0.12); + border: 1px solid rgba(255, 193, 7, 0.3); +} + +.soon-plan-card__head { + padding: 28px 22px 0; +} + +.soon-plan-card--badged .soon-plan-card__head { + padding-top: 40px; +} + +.soon-plan-card--save .soon-plan-card__head { + padding-top: 44px; +} + +.soon-plan-card__price-row { + display: flex; + align-items: baseline; + gap: 2px; + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.soon-plan-card__currency { + font-size: 15px; + font-weight: 600; + color: var(--soon-accent); + margin-top: 6px; +} + +.soon-plan-card__price { + font-size: 32px; + font-weight: 800; + color: var(--soon-text-strong); + line-height: 1; + letter-spacing: -0.03em; +} + +.soon-plan-card--featured .soon-plan-card__price { + color: #e0f2f1; +} + +.soon-plan-card__period { + margin-left: 4px; + font-size: 13px; + color: var(--soon-text-muted); + font-weight: 400; +} + +.soon-plan-card__body { + padding: 16px 22px 0; + flex: 1; +} + +.soon-plan-card__limits-title { + margin: 0 0 8px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--soon-text-muted); +} + +.soon-plan-card__features { + margin: 0; + padding: 0; + list-style: none; +} + +.soon-plan-card__features li { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 9px 0; + font-size: 13px; + color: var(--soon-text); + line-height: 1.45; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.soon-member-page .soon-plan-card__features li::before, +.soon-member-page .soon-member-dash__can li::before { + content: none; + display: none; +} + +.soon-plan-card__features li:last-child { + border-bottom: none; +} + +.soon-plan-card__check { + flex-shrink: 0; + width: 18px; + height: 18px; + margin-top: 1px; + border-radius: 50%; + background: rgba(0, 150, 136, 0.15); + display: flex; + align-items: center; + justify-content: center; +} + +.soon-plan-card__check svg { + width: 10px; + height: 10px; + stroke: var(--soon-accent); +} + +.soon-member-pager { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + font-size: 13px; + color: var(--soon-text-muted); +} + +.soon-plan-card__foot { + padding: 20px 22px 22px; + margin-top: auto; +} + +.soon-plan-card__cta { + width: 100%; + height: 42px; + font-size: 14px; + font-weight: 600; + border-radius: 10px; + letter-spacing: 0.02em; +} + +.soon-plan-card--featured .soon-plan-card__cta:not(:disabled) { + box-shadow: 0 4px 16px rgba(0, 150, 136, 0.35); +} + +.soon-plan-card__cta--ghost { + background: rgba(255, 255, 255, 0.04) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + color: var(--soon-text-muted) !important; + cursor: default; + box-shadow: none !important; +} + +.soon-plan-card__cta--ghost:hover { + background: rgba(255, 255, 255, 0.04) !important; + border-color: rgba(255, 255, 255, 0.1) !important; +} + +/* ── 订单表 ── */ +.soon-member-orders { + border-radius: 14px; + overflow: hidden; + background: rgba(54, 61, 69, 0.55); + border: 1px solid rgba(255, 255, 255, 0.07); + box-shadow: var(--soon-shadow-sm); +} + +.soon-member-orders table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.soon-member-orders th, +.soon-member-orders td { + padding: 14px 20px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.soon-member-orders th { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--soon-text-muted); + background: rgba(0, 0, 0, 0.2); +} + +.soon-member-orders tbody tr { + transition: background 0.15s; +} + +.soon-member-orders tbody tr:hover { + background: rgba(255, 255, 255, 0.03); +} + +.soon-member-orders tr:last-child td { + border-bottom: none; +} + +.soon-member-orders td:first-child { + font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; + font-size: 12px; + color: var(--soon-text-muted); +} + +.soon-member-orders__amount { + font-weight: 600; + color: var(--soon-text-strong); +} + +.soon-member-order-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; +} + +.soon-member-order-badge--paid { + background: rgba(102, 187, 106, 0.15); + color: #81c784; +} + +.soon-member-order-badge--pending { + background: rgba(255, 183, 77, 0.12); + color: var(--soon-warning); +} + +.soon-member-order-badge--cancelled, +.soon-member-order-badge--refunded { + background: rgba(255, 255, 255, 0.06); + color: var(--soon-text-muted); +} + +.soon-member-orders-empty { + padding: 48px 24px; + text-align: center; + color: var(--soon-text-muted); + font-size: 14px; +} + +.soon-member-order-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.soon-member-order-act--empty { + color: var(--soon-text-muted); +} + +.soon-member-order-act--muted { + opacity: 0.85; +} + +/* ── 支付弹窗 ── */ +.soon-pay-sheet { + padding: 0; + text-align: left; + overflow: hidden; + border-radius: 14px; +} + +.soon-pay-sheet__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + padding: 22px 26px 20px; + background: linear-gradient(160deg, rgba(0, 150, 136, 0.12) 0%, rgba(30, 36, 42, 0.95) 55%); + border-bottom: 1px solid rgba(255, 255, 255, 0.07); +} + +.soon-pay-sheet__eyebrow { + display: block; + margin-bottom: 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--soon-accent); +} + +.soon-pay-sheet__title { + margin: 0; + font-size: 20px; + font-weight: 700; + color: var(--soon-text-strong); + line-height: 1.3; +} + +.soon-pay-sheet__order { + margin: 8px 0 0; + font-size: 11px; + font-family: ui-monospace, Consolas, monospace; + color: var(--soon-text-muted); +} + +.soon-pay-sheet__head-price { + flex-shrink: 0; + text-align: right; + line-height: 1; +} + +.soon-pay-sheet__currency { + font-size: 18px; + font-weight: 600; + color: var(--soon-text-muted); + vertical-align: top; + margin-right: 2px; +} + +.soon-pay-sheet__amount { + font-size: 34px; + font-weight: 800; + color: var(--soon-text-strong); + letter-spacing: -0.03em; +} + +.soon-pay-sheet__body { + padding: 22px 26px 0; +} + +.soon-pay-stepper { + display: flex; + align-items: flex-start; + justify-content: center; + list-style: none; + margin: 0 0 24px; + padding: 0; +} + +.soon-pay-stepper__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: 72px; + flex-shrink: 0; +} + +.soon-pay-stepper__bridge { + flex: 1; + min-width: 24px; + max-width: 56px; + height: 2px; + margin-top: 15px; + background: rgba(255, 255, 255, 0.1); + border-radius: 1px; + list-style: none; +} + +.soon-pay-stepper__bridge.is-done { + background: linear-gradient(90deg, var(--soon-accent), rgba(77, 182, 172, 0.6)); +} + +.soon-pay-stepper__dot { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: var(--soon-text-muted); + background: rgba(0, 0, 0, 0.35); + border: 2px solid rgba(255, 255, 255, 0.12); + transition: background 0.2s, border-color 0.2s, color 0.2s; +} + +.soon-pay-stepper__label { + font-size: 11px; + color: var(--soon-text-muted); + text-align: center; + white-space: nowrap; +} + +.soon-pay-stepper__item.is-active .soon-pay-stepper__dot { + color: #fff; + background: var(--soon-accent); + border-color: var(--soon-accent); + box-shadow: 0 0 0 4px rgba(0, 150, 136, 0.18); +} + +.soon-pay-stepper__item.is-active .soon-pay-stepper__label { + color: var(--soon-text-strong); + font-weight: 600; +} + +.soon-pay-stepper__item.is-done .soon-pay-stepper__dot { + color: #fff; + background: rgba(77, 182, 172, 0.35); + border-color: var(--soon-accent); +} + +.soon-pay-stepper__item.is-done .soon-pay-stepper__label { + color: var(--soon-text); +} + +.soon-pay-section { + margin-bottom: 18px; +} + +.soon-pay-section__title { + margin: 0 0 12px; + font-size: 12px; + font-weight: 600; + color: var(--soon-text-muted); + letter-spacing: 0.06em; +} + +.soon-pay-channels { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.soon-pay-channel { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 12px 14px; + border: 1.5px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + cursor: pointer; + background: rgba(0, 0, 0, 0.18); + transition: border-color 0.2s, background 0.2s, box-shadow 0.2s; + text-align: left; + font: inherit; + color: inherit; +} + +.soon-pay-channel:hover { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(0, 0, 0, 0.24); +} + +.soon-pay-channel.is-active { + border-color: var(--soon-accent); + background: rgba(0, 150, 136, 0.07); + box-shadow: inset 0 0 0 1px rgba(0, 150, 136, 0.15); +} + +.soon-pay-channel[data-ch="wechat"].is-active { + border-color: #07c160; + background: rgba(7, 193, 96, 0.08); + box-shadow: inset 0 0 0 1px rgba(7, 193, 96, 0.2); +} + +.soon-pay-channel[data-ch="alipay"].is-active { + border-color: #1677ff; + background: rgba(22, 119, 255, 0.08); + box-shadow: inset 0 0 0 1px rgba(22, 119, 255, 0.2); +} + +.soon-pay-channel__radio { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.2); + flex-shrink: 0; + position: relative; +} + +.soon-pay-channel.is-active .soon-pay-channel__radio { + border-color: currentColor; +} + +.soon-pay-channel.is-active .soon-pay-channel__radio::after { + content: ''; + position: absolute; + inset: 3px; + border-radius: 50%; + background: currentColor; +} + +.soon-pay-channel[data-ch="wechat"].is-active .soon-pay-channel__radio { color: #07c160; } +.soon-pay-channel[data-ch="alipay"].is-active .soon-pay-channel__radio { color: #1677ff; } + +.soon-pay-channel__icon { + width: 34px; + height: 34px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 800; + color: #fff; + flex-shrink: 0; +} + +.soon-pay-channel__icon--wechat { background: linear-gradient(135deg, #06ae56, #07c160); } +.soon-pay-channel__icon--alipay { background: linear-gradient(135deg, #0958d9, #1677ff); } + +.soon-pay-channel__text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.soon-pay-channel__name { + font-size: 13px; + font-weight: 600; + color: var(--soon-text-strong); +} + +.soon-pay-channel__hint { + font-size: 11px; + color: var(--soon-text-muted); +} + +.soon-pay-qr-panel { + min-height: 168px; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + padding: 4px 0; +} + +.soon-pay-sheet--checkout .soon-pay-qr-panel { + min-height: 0; +} + +.soon-pay-sheet--checkout .soon-pay-stepper { + margin-bottom: 16px; +} + +.soon-pay-checkout-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 28px 20px; + border-radius: 12px; + background: rgba(0, 0, 0, 0.22); + border: 1px solid rgba(255, 255, 255, 0.06); + text-align: center; +} + +.soon-pay-checkout-empty__icon { + width: 48px; + height: 48px; + color: rgba(255, 255, 255, 0.22); + margin-bottom: 4px; +} + +.soon-pay-checkout-empty__icon svg { + width: 100%; + height: 100%; +} + +.soon-pay-checkout-empty__title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--soon-text); +} + +.soon-pay-checkout-empty__desc { + margin: 0; + font-size: 12px; + color: var(--soon-text-muted); + line-height: 1.55; + max-width: 260px; +} + +.soon-pay-checkout-loading { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 24px; + border-radius: 12px; + background: rgba(0, 0, 0, 0.22); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.soon-pay-checkout-loading p { + margin: 0; + font-size: 12px; + color: var(--soon-text-muted); +} + +.soon-pay-checkout-ready { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 22px 20px 18px; + border-radius: 12px; + background: rgba(0, 0, 0, 0.22); + border: 1px solid rgba(255, 255, 255, 0.08); + text-align: center; +} + +.soon-pay-checkout-ready--alipay { + padding: 18px 16px 14px; + gap: 8px; +} + +.soon-pay-checkout-ready__brand { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + font-weight: 800; + color: #fff; +} + +.soon-pay-checkout-ready__brand--wechat { background: linear-gradient(135deg, #06ae56, #07c160); } +.soon-pay-checkout-ready__brand--alipay { background: linear-gradient(135deg, #0958d9, #1677ff); } + +.soon-pay-checkout-ready__title { + margin: 0; + font-size: 15px; + font-weight: 600; + color: var(--soon-text-strong); +} + +.soon-pay-checkout-ready__desc { + margin: 0; + font-size: 12px; + color: var(--soon-text-muted); + line-height: 1.5; +} + +.soon-pay-checkout-ready__qr { + padding: 10px; + background: #fff; + border-radius: 12px; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35); + line-height: 0; +} + +.soon-pay-checkout-ready__qr canvas, +.soon-pay-checkout-ready__qr img { + display: block; + border-radius: 4px; +} + +.soon-pay-checkout-ready__btn { + min-width: 180px; + margin-top: 4px; +} + +.soon-pay-sheet__foot { + padding: 12px 26px 18px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + margin-top: 4px; +} + +.soon-pay-sheet--checkout .soon-pay-sheet__foot { + padding-top: 10px; + padding-bottom: 16px; +} + +.soon-pay-status { + margin: 0 0 12px; + text-align: center; + font-size: 12px; + color: var(--soon-text-muted); +} + +.soon-pay-status--ok { color: #4db6ac; } +.soon-pay-status--err { color: var(--soon-danger); } + +.soon-pay-sheet__cta { + width: 100%; +} + +.soon-pay-sheet__cta--secondary { + margin-top: 8px; +} + +.soon-member-page ::-webkit-scrollbar { width: 8px; height: 8px; } +.soon-member-page ::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.12); + border-radius: 4px; +} diff --git a/frontend-web/assets/css/portal.css b/frontend-web/assets/css/portal.css new file mode 100644 index 0000000..3d51c69 --- /dev/null +++ b/frontend-web/assets/css/portal.css @@ -0,0 +1,779 @@ +@import url('tokens.css'); +@import url('components.css'); + +.soon-portal-topbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 clamp(16px, 3vw, 28px); + background: var(--soon-bg-sidebar); + border-bottom: 1px solid var(--soon-border-subtle); + z-index: 1000; +} +.soon-portal-topbar__brand { + display: flex; + align-items: center; + gap: 10px; + color: var(--soon-text-strong); + font-weight: 600; + font-size: 17px; + letter-spacing: 0.02em; +} +.soon-portal-topbar__brand img { + width: 32px; + height: 32px; + border-radius: 8px; +} +.soon-portal-topbar__actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0; + min-height: 32px; +} + +.soon-portal-topbar__tools { + display: inline-flex; + align-items: center; + gap: 16px; +} + +.soon-portal-topbar__links { + display: inline-flex; + align-items: center; + gap: 16px; +} + +.soon-portal-topbar__account { + display: inline-flex; + align-items: center; + gap: 10px; + margin-left: 20px; + padding-left: 20px; + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +.soon-portal-topbar__session { + display: inline-flex; + align-items: center; + gap: 0; + margin-left: 20px; + padding-left: 20px; + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +.soon-portal-topbar__guest { + display: inline-flex; + align-items: center; + gap: 10px; + margin-left: 20px; + padding-left: 20px; + border-left: 1px solid rgba(255, 255, 255, 0.12); +} + +.soon-portal-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--soon-accent); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 14px; + font-weight: 600; + line-height: 1; +} + +.soon-portal-identity { + display: inline-flex; + align-items: center; + height: 32px; + font-size: 13px; + line-height: 1; + color: var(--soon-text-muted); + max-width: 260px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.soon-portal-identity--member { + color: var(--soon-accent); + font-weight: 600; +} +.soon-portal-identity--free { + color: var(--soon-text-muted); +} +.soon-portal-topbar a.soon-portal-topbar__link { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + padding: 0 10px; + color: var(--soon-text); + text-decoration: none; + font-size: var(--soon-font-sm); + line-height: 1; + white-space: nowrap; + transition: color 0.2s; +} + +.soon-portal-topbar__session .soon-portal-topbar__link + .soon-portal-topbar__link { + margin-left: 4px; + padding-left: 14px; + position: relative; +} + +.soon-portal-topbar__session .soon-portal-topbar__link + .soon-portal-topbar__link::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 14px; + background: rgba(255, 255, 255, 0.18); +} +.soon-portal-topbar a.soon-portal-topbar__link:hover { + color: var(--soon-accent); + text-decoration: none; +} +.soon-portal-topbar .soon-btn { + color: #fff; + text-decoration: none; +} +.soon-portal-topbar .soon-btn:hover { text-decoration: none; } +.soon-portal-topbar .soon-btn--ghost { + background: transparent; + box-shadow: none; + border-color: var(--soon-border-subtle); +} +.soon-portal-topbar .soon-btn--ghost:hover { + border-color: var(--soon-accent); + color: var(--soon-accent); + background: var(--soon-accent-soft); + box-shadow: none; +} + +.soon-section { + margin-bottom: var(--soon-section-gap); +} +.soon-section--recent { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +.soon-templates-section { + flex-shrink: 0; + margin-top: 0; + animation: soon-fadeInUp 0.4s var(--soon-ease); +} +.soon-section-title--bar { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.soon-section-title__main { + display: inline-flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.soon-section-title__lead { + flex: 1; + min-width: 160px; + margin: 0; + font-size: var(--soon-font-sm); + color: var(--soon-text-muted); + line-height: 1.5; +} + +.soon-templates-strip-wrap { + display: flex; + align-items: stretch; + gap: 8px; +} + +.soon-templates-strip { + flex: 1; + min-width: 0; + display: flex; + flex-wrap: nowrap; + align-items: stretch; + gap: 14px; + overflow: hidden; + padding: 4px 0 8px; +} + +.soon-templates-strip__nav { + flex-shrink: 0; + align-self: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--soon-border-subtle); + border-radius: var(--soon-radius-sm); + background: var(--soon-bg-elevated); + color: var(--soon-text-muted); + font-size: 20px; + line-height: 1; + cursor: pointer; + transition: color 0.15s, border-color 0.15s, background 0.15s; +} + +.soon-templates-strip__nav:hover:not(:disabled) { + color: var(--soon-text-strong); + border-color: var(--soon-accent); + background: var(--soon-accent-soft); +} + +.soon-templates-strip__nav:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.soon-plan-current__meta { color: var(--soon-text-muted); font-size: var(--soon-font-sm); margin: 0; } +.soon-plan-current__usage { color: var(--soon-text-muted); font-size: var(--soon-font-xs); margin-top: var(--soon-space-sm); } +.soon-progress__bar--warn { background: var(--soon-warning); } +.soon-plan-card__features { + margin: 0 0 var(--soon-space-lg); + padding: 0; + list-style: none; + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); +} +.soon-plan-card__features li { padding: var(--soon-space-xs) 0; } +.soon-plan-card__free { color: var(--soon-text-muted); font-size: var(--soon-font-sm); } + +.soon-pay-modal__title { color: var(--soon-text-strong); margin: 0 0 var(--soon-space-md); } +.soon-pay-channel[data-ch="wechat"].is-active, +.soon-pay-channel[data-ch="wechat"]:hover { border-color: #07c160; color: #07c160; } +.soon-pay-channel[data-ch="alipay"].is-active, +.soon-pay-channel[data-ch="alipay"]:hover { border-color: #1677ff; color: #1677ff; } + +.soon-member-page { + min-height: 100vh; + background: var(--soon-bg-deep); + font-family: var(--soon-font); + padding: calc(52px + var(--soon-space-xl)) var(--soon-space-xl) var(--soon-space-xl); + animation: soon-fadeInUp 0.4s; +} +.soon-member-wrap { max-width: 960px; margin: 0 auto; } +.soon-template-card { + flex: 1 1 0; + min-width: 0; + max-width: 148px; + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border-subtle); + border-radius: var(--soon-radius-sm); + overflow: hidden; + cursor: pointer; + transition: transform 0.2s var(--soon-ease), border-color 0.2s, box-shadow 0.2s; + animation: soon-fadeInUp 0.4s var(--soon-ease) backwards; +} + +.soon-templates-strip .soon-empty--inline { + flex: 1 1 100%; + width: 100%; + min-width: 100%; +} +.soon-template-card:hover { + border-color: var(--soon-accent); + background: var(--soon-bg-elevated); +} +.soon-template-card img { + width: 100%; + height: 100px; + object-fit: cover; + background: var(--soon-bg-deep); +} +.soon-template-card span { + display: block; + padding: var(--soon-space-sm); + font-size: 12px; + color: var(--soon-text); + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + +.soon-member-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--soon-space-xl); +} +.soon-member-header h1 { color: var(--soon-text-strong); margin: 0; font-size: 28px; } +.soon-plan-current { + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius); + padding: var(--soon-space-lg); + margin-bottom: var(--soon-space-xl); +} +.soon-plan-current h2 { color: var(--soon-text-strong); margin: 0 0 var(--soon-space-sm); font-size: 18px; } +.soon-progress { + height: 6px; + background: var(--soon-bg-deep); + border-radius: 3px; + margin-top: var(--soon-space-md); + overflow: hidden; +} +.soon-progress__bar { + height: 100%; + background: var(--soon-accent); + border-radius: 3px; + transition: width 0.3s; +} +.soon-plans-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: var(--soon-space-lg); +} +.soon-plan-card { + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius); + padding: var(--soon-space-lg); + position: relative; + transition: transform 0.2s, border-color 0.2s; +} +.soon-plan-card:hover { transform: translateY(-2px); border-color: var(--soon-accent); } +.soon-plan-card--featured { + border-color: var(--soon-accent); + box-shadow: 0 0 0 1px rgba(0, 150, 136, 0.3); +} +.soon-plan-card__badge { + position: absolute; + top: -12px; + right: 16px; + width: 48px; + height: 48px; +} +.soon-plan-card__name { color: var(--soon-text-strong); font-size: 18px; margin-bottom: var(--soon-space-sm); } +.soon-plan-card__price { color: var(--soon-accent); font-size: 28px; font-weight: 600; margin-bottom: var(--soon-space-md); } +.soon-plan-card__meta { color: var(--soon-text-muted); font-size: 13px; margin-bottom: var(--soon-space-lg); } + +.soon-pay-modal { padding: var(--soon-space-lg); text-align: center; } +.soon-pay-channels { display: flex; gap: var(--soon-space-md); justify-content: center; margin-bottom: var(--soon-space-lg); } +.soon-pay-channel { + padding: var(--soon-space-md) var(--soon-space-lg); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius); + cursor: pointer; + background: var(--soon-bg-panel); + color: var(--soon-text); + transition: border-color 0.2s; +} +.soon-pay-channel:hover, .soon-pay-channel.is-active { border-color: var(--soon-accent); color: var(--soon-accent); } +.soon-pay-qr { min-height: 200px; display: flex; align-items: center; justify-content: center; } +.soon-pay-qr img { max-width: 200px; } +.soon-pay-status { margin-top: var(--soon-space-md); color: var(--soon-text-muted); font-size: 13px; } + +.soon-home, .soon-home html, .soon-home body { + width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0; + background-color: var(--soon-bg-deep); + font-family: var(--soon-font); + -webkit-user-select: none; +} +.soon-home .layui-fluid.main { + height: 100%; + padding-top: 52px; + display: flex; + flex-direction: column; + box-sizing: border-box; +} +.soon-home .layui-row { height: 100%; display: flex; } +.soon-home .layui-col-md2 { + width: 240px; + flex-shrink: 0; + background: var(--soon-bg-sidebar); + border-right: 1px solid var(--soon-border-subtle); + height: 100%; + overflow-y: auto; +} +.soon-home-sidebar { + padding: 20px 16px; + display: flex; + flex-direction: column; + gap: 12px; +} +.soon-home-sidebar__title { + font-size: var(--soon-font-md); + font-weight: 600; + color: var(--soon-text-strong); + margin: 0 0 8px; + padding: 0; + border: none; +} +.soon-home-new-card { + cursor: pointer; + border-radius: var(--soon-radius-sm); + background: var(--soon-bg-panel); + border: 1px solid transparent; + padding: 14px 12px; + transition: background 0.2s var(--soon-ease), border-color 0.2s; +} +.soon-home-new-card:hover { + background: var(--soon-bg-elevated); + border-color: var(--soon-border); +} +.soon-home-new-card .rect { + width: 100%; + height: auto; + min-height: 0; + background: transparent; + border: none; + border-radius: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} +.soon-home-new-card .rect img { + width: 72px; + height: 72px; + object-fit: contain; + margin: 0; +} +.soon-home-new-card .tip { + color: var(--soon-text); + font-size: var(--soon-font-sm); + font-weight: 400; + text-align: center; + line-height: 1.4; +} +.soon-home-new-card:hover .tip { color: var(--soon-text-strong); } +.soon-home .left .card { margin: 0; } +.soon-home .layui-col-md10 { + flex: 1; + background: var(--soon-bg-content); + height: 100%; + overflow: hidden; +} +.soon-home .right { + padding: 20px 24px; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + gap: 16px; + box-sizing: border-box; +} +.soon-home-panel { + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border-subtle); + border-radius: var(--soon-radius); + padding: 16px; + display: flex; + flex-direction: column; + min-height: 0; +} +.soon-section--recent.soon-home-panel { + flex: 1; + min-height: 430px; +} + +.soon-templates-section.soon-home-panel { flex-shrink: 0; } + +.soon-home .card-list, +.soon-home-recent-grid { + margin-top: 0; + flex: 1 1 auto; + min-height: 340px; + overflow: hidden; + display: grid; + grid-template-columns: repeat(auto-fill, 168px); + gap: 16px; + align-content: start; + justify-content: start; + padding: 4px 2px 8px; +} + +.soon-home .card-list > .card, +.soon-home-recent-grid > .card { + width: 168px; + max-width: 168px; +} + +.soon-section--recent #filePager { + flex-shrink: 0; + margin-top: auto; + padding-top: 10px; + border-top: 1px solid var(--soon-border-subtle); +} + +.soon-home .card-list .rect { + width: 168px; + height: 160px; + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border-subtle); + border-radius: var(--soon-radius-sm); + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s; + position: relative; + cursor: pointer; + overflow: hidden; + box-sizing: border-box; +} +.soon-home .card-list .rect:hover { + border-color: var(--soon-accent); + background: var(--soon-bg-elevated); +} +.soon-home .card-list .rect img { + display: block; + width: 100%; + height: 135px; + flex-shrink: 0; + object-fit: contain; + object-position: center center; + margin: 0; + background: var(--soon-bg-deep); + padding: 2px; + box-sizing: border-box; + border-radius: 4px; + transition: transform 0.3s; +} +.soon-home .card-list .rect1 img { transform: scale(0.8); } +.soon-home .card-list .rect1:hover img { transform: scale(0.85); } +.soon-home .card-list .tip { + flex-shrink: 0; + width: 100%; + height: 25px; + line-height: 25px; + color: var(--soon-text-muted); + font-size: 12px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 8px; + box-sizing: border-box; + border-top: 1px solid var(--soon-border-subtle); + background: rgba(0, 0, 0, 0.15); +} +.soon-home .card-list .rect:hover .tip { color: var(--soon-text-strong); } +.soon-home .card-list .soon-file-card__actions { + position: absolute; + top: 6px; + right: 6px; + z-index: 4; + display: flex; + align-items: center; + gap: 2px; + padding: 3px; + border-radius: 9px; + background: rgba(16, 20, 24, 0.82); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32); + backdrop-filter: blur(8px); + opacity: 0; + pointer-events: none; + transform: translateY(-2px); + transition: opacity 0.2s, transform 0.2s; +} +.soon-home .card-list .rect:hover .soon-file-card__actions { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} +.soon-home .soon-file-card__action { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + border: none; + border-radius: 6px; + background: transparent; + color: rgba(255, 255, 255, 0.88); + cursor: pointer; + transition: background 0.15s, color 0.15s, transform 0.15s; +} +.soon-home .soon-file-card__action:active { + transform: scale(0.94); +} +.soon-home .soon-file-card__icon { + display: block; + fill: none; + stroke: currentColor; + stroke-width: 1.75; + stroke-linecap: round; + stroke-linejoin: round; +} +.soon-home .soon-file-card__download:hover { + background: rgba(0, 150, 136, 0.22); + color: #b2dfdb; +} +.soon-home .soon-file-card__delete:hover { + background: rgba(229, 115, 115, 0.2); + color: #ffcdd2; +} +.soon-home .rect:not(.rect1) { position: relative; } +.soon-home-recent-grid .soon-empty--inline { + grid-column: 1 / -1; + width: 100%; + max-width: none; +} + +.soon-home .card-list .rect:not(.rect1)::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, calc(-50% - 12.5px)); + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--soon-bg-panel); + box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8); + border: 1px solid rgba(255, 255, 255, 0.1); + z-index: 3; + pointer-events: none; +} +.soon-portal-topbar__brand a { + display: flex; + align-items: center; + gap: var(--soon-space-sm); + color: inherit; + text-decoration: none; +} +.soon-portal-topbar__brand a:hover { color: inherit; text-decoration: none; } + +.soon-portal-topbar select, +.soon-portal-topbar__lang { + display: inline-flex; + align-items: center; + margin: 0; + background: var(--soon-bg-panel); + color: var(--soon-text); + border: 1px solid var(--soon-border-subtle); + height: 32px; + border-radius: var(--soon-radius-sm); + padding: 0 28px 0 10px; + font-size: var(--soon-font-sm); + line-height: 1; + cursor: pointer; + box-sizing: border-box; + vertical-align: middle; +} + +.soon-pay-banner.is-visible { display: block; } +.soon-pay-banner { display: none; } + +.soon-input--textarea { + height: auto; + min-height: 88px; + padding: 8px 12px; + resize: vertical; + line-height: 1.5; +} + +.soon-home ::-webkit-scrollbar, +.soon-member-page ::-webkit-scrollbar, +.soon-admin-layout ::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.soon-home ::-webkit-scrollbar-track, +.soon-member-page ::-webkit-scrollbar-track, +.soon-admin-layout ::-webkit-scrollbar-track { + background: #2a2a2a; + border-radius: 4px; +} +.soon-home ::-webkit-scrollbar-thumb, +.soon-member-page ::-webkit-scrollbar-thumb, +.soon-admin-layout ::-webkit-scrollbar-thumb { + background: #555; + border-radius: 4px; +} + +@media (max-width: 768px) { + .soon-home .layui-row { + flex-direction: column; + overflow-y: auto; + } + .soon-home .layui-col-md2, + .soon-home .layui-col-md10 { + width: 100%; + flex: none; + height: auto; + min-height: 0; + } + .soon-home .layui-col-md10 { + flex: 1; + min-height: 280px; + } + .soon-section-title--bar { + flex-direction: column; + align-items: stretch; + } + .soon-section-title--bar .soon-btn { + align-self: flex-end; + } + .soon-home-recent-grid { + grid-template-columns: repeat(auto-fill, 148px); + min-height: 300px; + } + .soon-home .card-list > .card, + .soon-home-recent-grid > .card { + width: 148px; + max-width: 148px; + } + .soon-home .card-list .rect { + width: 148px; + height: 150px; + } + .soon-home .card-list .rect img { + height: 125px; + } + .soon-home .card-list .rect:not(.rect1)::after { + transform: translate(-50%, calc(-50% - 12px)); + width: 20px; + height: 20px; + } + .soon-portal-topbar { + padding: 0 var(--soon-space-md); + gap: var(--soon-space-sm); + } + .soon-portal-topbar__actions { + gap: var(--soon-space-sm); + flex-wrap: wrap; + justify-content: flex-end; + } +} + +.soon-file-pager { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin: 0; + padding: 0; + font-size: 13px; + color: var(--soon-text-muted); +} +.soon-file-pager__info { + min-width: 140px; + text-align: center; +} diff --git a/frontend-web/assets/css/subscribe-gate.css b/frontend-web/assets/css/subscribe-gate.css new file mode 100644 index 0000000..f481019 --- /dev/null +++ b/frontend-web/assets/css/subscribe-gate.css @@ -0,0 +1,224 @@ +.soon-subscribe-modal { + text-align: left; + background: var(--soon-bg-panel); + color: var(--soon-text); +} + +.soon-subscribe-modal__head { + padding: 20px 48px 16px 22px; + border-bottom: 1px solid var(--soon-border-subtle); + background: linear-gradient(160deg, rgba(0, 150, 136, 0.1) 0%, transparent 100%); +} + +.soon-subscribe-modal__eyebrow { + display: block; + margin-bottom: 8px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.08em; + color: var(--soon-accent); +} + +.soon-subscribe-modal__title { + margin: 0; + font-size: 19px; + font-weight: 700; + color: var(--soon-text-strong); + line-height: 1.35; +} + +.soon-subscribe-modal__desc { + margin: 6px 0 0; + font-size: 13px; + line-height: 1.55; + color: var(--soon-text-muted); +} + +.soon-subscribe-modal__features { + margin: 12px 0 0; + padding: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 6px 16px; +} + +.soon-subscribe-modal__features li { + font-size: 12px; + color: var(--soon-text); + padding-left: 11px; + position: relative; +} + +.soon-subscribe-modal__features li::before { + content: ''; + position: absolute; + left: 0; + top: 7px; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--soon-accent); +} + +.soon-subscribe-modal__body { + padding: 16px 22px 18px; +} + +.soon-subscribe-plans__loading, +.soon-subscribe-plans__empty { + margin: 0; + padding: 24px 0; + text-align: center; + font-size: 14px; + color: var(--soon-text-muted); + grid-column: 1 / -1; +} + +.soon-subscribe-modal__section-head { + margin: 0 0 12px; +} + +.soon-subscribe-modal__section-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 4px; + font-size: 15px; + font-weight: 600; + color: var(--soon-text-strong); +} + +.soon-subscribe-modal__section-mark { + display: inline-block; + width: 4px; + height: 16px; + border-radius: 2px; + background: linear-gradient(180deg, var(--soon-accent), #26a69a); +} + +.soon-subscribe-modal__section-sub { + margin: 0; + padding-left: 12px; + font-size: 12px; + line-height: 1.5; + color: var(--soon-text-muted); +} + +.soon-subscribe-plans-grid { + gap: 14px; + align-items: stretch; +} + +@media (max-width: 860px) { + .soon-subscribe-plans-grid { + grid-template-columns: 1fr; + max-width: 360px; + margin: 0 auto; + } +} + +.soon-subscribe-modal .soon-plan-card--picker { + width: 100%; + margin: 0; + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.07); + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + animation: none; +} + +.soon-subscribe-modal .soon-plan-card--gate { + overflow: hidden; +} + +.soon-subscribe-modal .soon-plan-card--gate .soon-plan-card__head { + padding: 20px 16px 16px; +} + +.soon-subscribe-modal .soon-plan-card--badged.soon-plan-card--gate .soon-plan-card__head { + padding-top: 36px; +} + +.soon-subscribe-modal .soon-plan-card--save.soon-plan-card--gate .soon-plan-card__head { + padding-top: 40px; +} + +.soon-subscribe-modal .soon-plan-card--gate .soon-plan-card__name { + font-size: 16px; + margin-bottom: 4px; +} + +.soon-subscribe-modal .soon-plan-card--gate .soon-plan-card__desc { + font-size: 12px; + line-height: 1.45; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.soon-subscribe-modal .soon-plan-card--gate .soon-plan-card__price-row { + margin-top: 10px; + padding-top: 10px; +} + +.soon-subscribe-modal .soon-plan-card--gate .soon-plan-card__price { + font-size: 26px; +} + +.soon-subscribe-modal .soon-plan-card--picker:hover { + transform: none; + border-color: rgba(0, 150, 136, 0.35); +} + +.soon-subscribe-modal .soon-plan-card--picker.soon-plan-card--selected { + border-color: var(--soon-accent); + box-shadow: 0 0 0 1px rgba(0, 150, 136, 0.35); + background: linear-gradient(165deg, rgba(0, 150, 136, 0.1) 0%, rgba(54, 61, 69, 0.85) 45%); +} + +.soon-subscribe-modal__foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 22px 18px; + border-top: 1px solid var(--soon-border-subtle); + background: rgba(0, 0, 0, 0.12); +} + +.soon-subscribe-modal__stay { + flex-shrink: 0; + padding: 8px 16px; + border: 1px solid var(--soon-border-subtle); + border-radius: 8px; + background: transparent; + color: var(--soon-text-muted); + font-size: 13px; + line-height: 1.4; + cursor: pointer; + transition: color 0.15s, border-color 0.15s, background 0.15s; +} + +.soon-subscribe-modal__stay:hover { + color: var(--soon-text); + border-color: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.04); +} + +.soon-subscribe-modal__pay { + min-width: 160px; + padding: 10px 22px; + font-size: 14px; + font-weight: 600; +} + +.soon-subscribe-modal__pay:disabled { + opacity: 0.45; + cursor: not-allowed; +} diff --git a/frontend-web/assets/css/tokens.css b/frontend-web/assets/css/tokens.css new file mode 100644 index 0000000..76de900 --- /dev/null +++ b/frontend-web/assets/css/tokens.css @@ -0,0 +1,54 @@ +:root { + --soon-bg-deep: #2b3036; + --soon-bg-panel: #363d45; + --soon-bg-elevated: #3e454d; + --soon-bg-content: #31373d; + --soon-bg-sidebar: #2f353b; + --soon-text: #b8c0c8; + --soon-text-muted: #8a939c; + --soon-text-strong: #eef1f4; + --soon-accent: #009688; + --soon-accent-hover: #00a896; + --soon-accent-soft: rgba(0, 150, 136, 0.12); + --soon-border-subtle: rgba(255, 255, 255, 0.08); + --soon-danger: #e57373; + --soon-success: #66bb6a; + --soon-warning: #ffb74d; + --soon-border: #4a525a; + --soon-radius: 8px; + --soon-radius-sm: 6px; + --soon-radius-lg: 10px; + --soon-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); + --soon-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2); + --soon-ease: cubic-bezier(0.4, 0, 0.2, 1); + --soon-font: "Helvetica Neue", Helvetica, Arial, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; + --soon-font-xs: 12px; + --soon-font-sm: 13px; + --soon-font-md: 14px; + --soon-font-lg: 16px; + --soon-btn-h-sm: 32px; + --soon-input-h-sm: 36px; + --soon-focus-ring: 0 0 0 2px rgba(0, 150, 136, 0.35); + --soon-section-gap: 20px; + --soon-space-xs: 4px; + --soon-space-sm: 8px; + --soon-space-md: 16px; + --soon-space-lg: 24px; + --soon-space-xl: 32px; +} + +@keyframes soon-fadeInUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes soon-shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes soon-shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-4px); } + 75% { transform: translateX(4px); } +} diff --git a/frontend-web/assets/deploy-config.js b/frontend-web/assets/deploy-config.js new file mode 100644 index 0000000..d8cb60b --- /dev/null +++ b/frontend-web/assets/deploy-config.js @@ -0,0 +1,7 @@ +window.SOON_DEPLOY_CONFIG = { + api_v1_base: 'http://localhost:8102/api/v1', + api_admin_base: 'http://localhost:8102/api/admin', + front_base_url: 'http://localhost:8100', + web_base: 'http://localhost:8100', + version: 'local-dev' +}; diff --git a/frontend-web/assets/deploy-config.json b/frontend-web/assets/deploy-config.json new file mode 100644 index 0000000..1834ec3 --- /dev/null +++ b/frontend-web/assets/deploy-config.json @@ -0,0 +1,7 @@ +{ + "api_v1_base": "https://designadmin.cardsoon.com/api/v1", + "api_admin_base": "https://designadmin.cardsoon.com/api/admin", + "front_base_url": "https://design.cardsoon.com", + "web_base": "https://design.cardsoon.com", + "version": "1.0.0" +} diff --git a/frontend-web/assets/help/User Manual.pdf b/frontend-web/assets/help/User Manual.pdf new file mode 100644 index 0000000..b66d064 Binary files /dev/null and b/frontend-web/assets/help/User Manual.pdf differ diff --git a/frontend-web/assets/images/IconA.png b/frontend-web/assets/images/IconA.png new file mode 100644 index 0000000..5e3f64d Binary files /dev/null and b/frontend-web/assets/images/IconA.png differ diff --git a/frontend-web/assets/images/IconA256.ico b/frontend-web/assets/images/IconA256.ico new file mode 100644 index 0000000..b982a2c Binary files /dev/null and b/frontend-web/assets/images/IconA256.ico differ diff --git a/frontend-web/assets/images/IconA256.png b/frontend-web/assets/images/IconA256.png new file mode 100644 index 0000000..a1b7045 Binary files /dev/null and b/frontend-web/assets/images/IconA256.png differ diff --git a/frontend-web/assets/images/IconA64.png b/frontend-web/assets/images/IconA64.png new file mode 100644 index 0000000..f4e8bb2 Binary files /dev/null and b/frontend-web/assets/images/IconA64.png differ diff --git a/frontend-web/assets/images/about24.png b/frontend-web/assets/images/about24.png new file mode 100644 index 0000000..2dda7b5 Binary files /dev/null and b/frontend-web/assets/images/about24.png differ diff --git a/frontend-web/assets/images/back_bg_1.png b/frontend-web/assets/images/back_bg_1.png new file mode 100644 index 0000000..64497ca Binary files /dev/null and b/frontend-web/assets/images/back_bg_1.png differ diff --git a/frontend-web/assets/images/back_bg_2.png b/frontend-web/assets/images/back_bg_2.png new file mode 100644 index 0000000..a2c154a Binary files /dev/null and b/frontend-web/assets/images/back_bg_2.png differ diff --git a/frontend-web/assets/images/back_bg_bak_1.png b/frontend-web/assets/images/back_bg_bak_1.png new file mode 100644 index 0000000..d0565ab Binary files /dev/null and b/frontend-web/assets/images/back_bg_bak_1.png differ diff --git a/frontend-web/assets/images/back_bg_bak_2.png b/frontend-web/assets/images/back_bg_bak_2.png new file mode 100644 index 0000000..a2c154a Binary files /dev/null and b/frontend-web/assets/images/back_bg_bak_2.png differ diff --git a/frontend-web/assets/images/barcode.svg b/frontend-web/assets/images/barcode.svg new file mode 100644 index 0000000..8f7526e --- /dev/null +++ b/frontend-web/assets/images/barcode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/barcode1.png b/frontend-web/assets/images/barcode1.png new file mode 100644 index 0000000..753e4ea Binary files /dev/null and b/frontend-web/assets/images/barcode1.png differ diff --git a/frontend-web/assets/images/barcode32.png b/frontend-web/assets/images/barcode32.png new file mode 100644 index 0000000..3c71950 Binary files /dev/null and b/frontend-web/assets/images/barcode32.png differ diff --git a/frontend-web/assets/images/bg_1.png b/frontend-web/assets/images/bg_1.png new file mode 100644 index 0000000..36fcc2e Binary files /dev/null and b/frontend-web/assets/images/bg_1.png differ diff --git a/frontend-web/assets/images/bg_2.png b/frontend-web/assets/images/bg_2.png new file mode 100644 index 0000000..e5918d3 Binary files /dev/null and b/frontend-web/assets/images/bg_2.png differ diff --git a/frontend-web/assets/images/bg_back.png b/frontend-web/assets/images/bg_back.png new file mode 100644 index 0000000..60e3046 Binary files /dev/null and b/frontend-web/assets/images/bg_back.png differ diff --git a/frontend-web/assets/images/bg_back_1.png b/frontend-web/assets/images/bg_back_1.png new file mode 100644 index 0000000..60e3046 Binary files /dev/null and b/frontend-web/assets/images/bg_back_1.png differ diff --git a/frontend-web/assets/images/bg_back_2.png b/frontend-web/assets/images/bg_back_2.png new file mode 100644 index 0000000..c4dae02 Binary files /dev/null and b/frontend-web/assets/images/bg_back_2.png differ diff --git a/frontend-web/assets/images/bg_back_bak_1.png b/frontend-web/assets/images/bg_back_bak_1.png new file mode 100644 index 0000000..0d38cbc Binary files /dev/null and b/frontend-web/assets/images/bg_back_bak_1.png differ diff --git a/frontend-web/assets/images/bg_back_bak_2.png b/frontend-web/assets/images/bg_back_bak_2.png new file mode 100644 index 0000000..a742d8e Binary files /dev/null and b/frontend-web/assets/images/bg_back_bak_2.png differ diff --git a/frontend-web/assets/images/bg_bak_1.png b/frontend-web/assets/images/bg_bak_1.png new file mode 100644 index 0000000..a0afdf4 Binary files /dev/null and b/frontend-web/assets/images/bg_bak_1.png differ diff --git a/frontend-web/assets/images/bg_bak_2.png b/frontend-web/assets/images/bg_bak_2.png new file mode 100644 index 0000000..a742d8e Binary files /dev/null and b/frontend-web/assets/images/bg_bak_2.png differ diff --git a/frontend-web/assets/images/bg_front_1.png b/frontend-web/assets/images/bg_front_1.png new file mode 100644 index 0000000..7e6bcc3 Binary files /dev/null and b/frontend-web/assets/images/bg_front_1.png differ diff --git a/frontend-web/assets/images/bg_front_2.png b/frontend-web/assets/images/bg_front_2.png new file mode 100644 index 0000000..b104832 Binary files /dev/null and b/frontend-web/assets/images/bg_front_2.png differ diff --git a/frontend-web/assets/images/bg_front_bak_1.png b/frontend-web/assets/images/bg_front_bak_1.png new file mode 100644 index 0000000..6b9d4cd Binary files /dev/null and b/frontend-web/assets/images/bg_front_bak_1.png differ diff --git a/frontend-web/assets/images/bg_front_bak_2.png b/frontend-web/assets/images/bg_front_bak_2.png new file mode 100644 index 0000000..a742d8e Binary files /dev/null and b/frontend-web/assets/images/bg_front_bak_2.png differ diff --git a/frontend-web/assets/images/bold24.png b/frontend-web/assets/images/bold24.png new file mode 100644 index 0000000..172f340 Binary files /dev/null and b/frontend-web/assets/images/bold24.png differ diff --git a/frontend-web/assets/images/bottom24.png b/frontend-web/assets/images/bottom24.png new file mode 100644 index 0000000..d9368b1 Binary files /dev/null and b/frontend-web/assets/images/bottom24.png differ diff --git a/frontend-web/assets/images/center_x24.png b/frontend-web/assets/images/center_x24.png new file mode 100644 index 0000000..4fe6720 Binary files /dev/null and b/frontend-web/assets/images/center_x24.png differ diff --git a/frontend-web/assets/images/center_y24.png b/frontend-web/assets/images/center_y24.png new file mode 100644 index 0000000..8ee7796 Binary files /dev/null and b/frontend-web/assets/images/center_y24.png differ diff --git a/frontend-web/assets/images/circle24.png b/frontend-web/assets/images/circle24.png new file mode 100644 index 0000000..6b8b936 Binary files /dev/null and b/frontend-web/assets/images/circle24.png differ diff --git a/frontend-web/assets/images/circle32.png b/frontend-web/assets/images/circle32.png new file mode 100644 index 0000000..091245c Binary files /dev/null and b/frontend-web/assets/images/circle32.png differ diff --git a/frontend-web/assets/images/clean24.png b/frontend-web/assets/images/clean24.png new file mode 100644 index 0000000..8db6a33 Binary files /dev/null and b/frontend-web/assets/images/clean24.png differ diff --git a/frontend-web/assets/images/copy24.png b/frontend-web/assets/images/copy24.png new file mode 100644 index 0000000..384de8c Binary files /dev/null and b/frontend-web/assets/images/copy24.png differ diff --git a/frontend-web/assets/images/counter.svg b/frontend-web/assets/images/counter.svg new file mode 100644 index 0000000..47b98ac --- /dev/null +++ b/frontend-web/assets/images/counter.svg @@ -0,0 +1 @@ + 123 \ No newline at end of file diff --git a/frontend-web/assets/images/counter32.png b/frontend-web/assets/images/counter32.png new file mode 100644 index 0000000..ba97e46 Binary files /dev/null and b/frontend-web/assets/images/counter32.png differ diff --git a/frontend-web/assets/images/delete24.png b/frontend-web/assets/images/delete24.png new file mode 100644 index 0000000..ce5d258 Binary files /dev/null and b/frontend-web/assets/images/delete24.png differ diff --git a/frontend-web/assets/images/f1_objects.png b/frontend-web/assets/images/f1_objects.png new file mode 100644 index 0000000..f06c212 Binary files /dev/null and b/frontend-web/assets/images/f1_objects.png differ diff --git a/frontend-web/assets/images/favicon.icns b/frontend-web/assets/images/favicon.icns new file mode 100644 index 0000000..acab2a7 Binary files /dev/null and b/frontend-web/assets/images/favicon.icns differ diff --git a/frontend-web/assets/images/favicon.ico b/frontend-web/assets/images/favicon.ico new file mode 100644 index 0000000..b982a2c Binary files /dev/null and b/frontend-web/assets/images/favicon.ico differ diff --git a/frontend-web/assets/images/favicon.svg b/frontend-web/assets/images/favicon.svg new file mode 100644 index 0000000..a7822d8 --- /dev/null +++ b/frontend-web/assets/images/favicon.svg @@ -0,0 +1,583 @@ + + + + diff --git a/frontend-web/assets/images/front_bg1_1.png b/frontend-web/assets/images/front_bg1_1.png new file mode 100644 index 0000000..1098c5c Binary files /dev/null and b/frontend-web/assets/images/front_bg1_1.png differ diff --git a/frontend-web/assets/images/front_bg1_2.png b/frontend-web/assets/images/front_bg1_2.png new file mode 100644 index 0000000..f5e6084 Binary files /dev/null and b/frontend-web/assets/images/front_bg1_2.png differ diff --git a/frontend-web/assets/images/front_bg2_1.png b/frontend-web/assets/images/front_bg2_1.png new file mode 100644 index 0000000..55bdb4a Binary files /dev/null and b/frontend-web/assets/images/front_bg2_1.png differ diff --git a/frontend-web/assets/images/front_bg2_2.png b/frontend-web/assets/images/front_bg2_2.png new file mode 100644 index 0000000..16d17fc Binary files /dev/null and b/frontend-web/assets/images/front_bg2_2.png differ diff --git a/frontend-web/assets/images/front_bg_bak_1.png b/frontend-web/assets/images/front_bg_bak_1.png new file mode 100644 index 0000000..d0565ab Binary files /dev/null and b/frontend-web/assets/images/front_bg_bak_1.png differ diff --git a/frontend-web/assets/images/front_bg_bak_2.png b/frontend-web/assets/images/front_bg_bak_2.png new file mode 100644 index 0000000..e8b901d Binary files /dev/null and b/frontend-web/assets/images/front_bg_bak_2.png differ diff --git a/frontend-web/assets/images/help24.png b/frontend-web/assets/images/help24.png new file mode 100644 index 0000000..db200d1 Binary files /dev/null and b/frontend-web/assets/images/help24.png differ diff --git a/frontend-web/assets/images/image24.png b/frontend-web/assets/images/image24.png new file mode 100644 index 0000000..13085c2 Binary files /dev/null and b/frontend-web/assets/images/image24.png differ diff --git a/frontend-web/assets/images/image32.png b/frontend-web/assets/images/image32.png new file mode 100644 index 0000000..578c08a Binary files /dev/null and b/frontend-web/assets/images/image32.png differ diff --git a/frontend-web/assets/images/images.svg b/frontend-web/assets/images/images.svg new file mode 100644 index 0000000..a245705 --- /dev/null +++ b/frontend-web/assets/images/images.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/images_add.svg b/frontend-web/assets/images/images_add.svg new file mode 100644 index 0000000..a28d9bb --- /dev/null +++ b/frontend-web/assets/images/images_add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/italics24.png b/frontend-web/assets/images/italics24.png new file mode 100644 index 0000000..e85e322 Binary files /dev/null and b/frontend-web/assets/images/italics24.png differ diff --git a/frontend-web/assets/images/label24.png b/frontend-web/assets/images/label24.png new file mode 100644 index 0000000..227e798 Binary files /dev/null and b/frontend-web/assets/images/label24.png differ diff --git a/frontend-web/assets/images/label32.png b/frontend-web/assets/images/label32.png new file mode 100644 index 0000000..3e4dcc8 Binary files /dev/null and b/frontend-web/assets/images/label32.png differ diff --git a/frontend-web/assets/images/left24.png b/frontend-web/assets/images/left24.png new file mode 100644 index 0000000..4f6ea8e Binary files /dev/null and b/frontend-web/assets/images/left24.png differ diff --git a/frontend-web/assets/images/line.svg b/frontend-web/assets/images/line.svg new file mode 100644 index 0000000..98f6115 --- /dev/null +++ b/frontend-web/assets/images/line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/line24.png b/frontend-web/assets/images/line24.png new file mode 100644 index 0000000..644615f Binary files /dev/null and b/frontend-web/assets/images/line24.png differ diff --git a/frontend-web/assets/images/line32.png b/frontend-web/assets/images/line32.png new file mode 100644 index 0000000..2302e62 Binary files /dev/null and b/frontend-web/assets/images/line32.png differ diff --git a/frontend-web/assets/images/linethrough24.png b/frontend-web/assets/images/linethrough24.png new file mode 100644 index 0000000..d6f4fbd Binary files /dev/null and b/frontend-web/assets/images/linethrough24.png differ diff --git a/frontend-web/assets/images/lock20.png b/frontend-web/assets/images/lock20.png new file mode 100644 index 0000000..29be579 Binary files /dev/null and b/frontend-web/assets/images/lock20.png differ diff --git a/frontend-web/assets/images/logo.ico b/frontend-web/assets/images/logo.ico new file mode 100644 index 0000000..36486b9 Binary files /dev/null and b/frontend-web/assets/images/logo.ico differ diff --git a/frontend-web/assets/images/logo.png b/frontend-web/assets/images/logo.png new file mode 100644 index 0000000..c08b55e Binary files /dev/null and b/frontend-web/assets/images/logo.png differ diff --git a/frontend-web/assets/images/logo256.ico b/frontend-web/assets/images/logo256.ico new file mode 100644 index 0000000..3b07dc0 Binary files /dev/null and b/frontend-web/assets/images/logo256.ico differ diff --git a/frontend-web/assets/images/newfile.png b/frontend-web/assets/images/newfile.png new file mode 100644 index 0000000..1fd4163 Binary files /dev/null and b/frontend-web/assets/images/newfile.png differ diff --git a/frontend-web/assets/images/newfile24.png b/frontend-web/assets/images/newfile24.png new file mode 100644 index 0000000..41ede94 Binary files /dev/null and b/frontend-web/assets/images/newfile24.png differ diff --git a/frontend-web/assets/images/next.png b/frontend-web/assets/images/next.png new file mode 100644 index 0000000..4571ea5 Binary files /dev/null and b/frontend-web/assets/images/next.png differ diff --git a/frontend-web/assets/images/next24.png b/frontend-web/assets/images/next24.png new file mode 100644 index 0000000..2eee259 Binary files /dev/null and b/frontend-web/assets/images/next24.png differ diff --git a/frontend-web/assets/images/op_1.png b/frontend-web/assets/images/op_1.png new file mode 100644 index 0000000..a016070 Binary files /dev/null and b/frontend-web/assets/images/op_1.png differ diff --git a/frontend-web/assets/images/op_2.png b/frontend-web/assets/images/op_2.png new file mode 100644 index 0000000..ac1094d Binary files /dev/null and b/frontend-web/assets/images/op_2.png differ diff --git a/frontend-web/assets/images/op_bak_1.png b/frontend-web/assets/images/op_bak_1.png new file mode 100644 index 0000000..a016070 Binary files /dev/null and b/frontend-web/assets/images/op_bak_1.png differ diff --git a/frontend-web/assets/images/op_bak_2.png b/frontend-web/assets/images/op_bak_2.png new file mode 100644 index 0000000..09748a6 Binary files /dev/null and b/frontend-web/assets/images/op_bak_2.png differ diff --git a/frontend-web/assets/images/open.png b/frontend-web/assets/images/open.png new file mode 100644 index 0000000..4ee4821 Binary files /dev/null and b/frontend-web/assets/images/open.png differ diff --git a/frontend-web/assets/images/open24.png b/frontend-web/assets/images/open24.png new file mode 100644 index 0000000..ae3c620 Binary files /dev/null and b/frontend-web/assets/images/open24.png differ diff --git a/frontend-web/assets/images/outimage24.png b/frontend-web/assets/images/outimage24.png new file mode 100644 index 0000000..4c5bb71 Binary files /dev/null and b/frontend-web/assets/images/outimage24.png differ diff --git a/frontend-web/assets/images/outimage32.png b/frontend-web/assets/images/outimage32.png new file mode 100644 index 0000000..dd1fc90 Binary files /dev/null and b/frontend-web/assets/images/outimage32.png differ diff --git a/frontend-web/assets/images/outpic200.png b/frontend-web/assets/images/outpic200.png new file mode 100644 index 0000000..78708f8 Binary files /dev/null and b/frontend-web/assets/images/outpic200.png differ diff --git a/frontend-web/assets/images/outpic_extra.png b/frontend-web/assets/images/outpic_extra.png new file mode 100644 index 0000000..f15294c Binary files /dev/null and b/frontend-web/assets/images/outpic_extra.png differ diff --git a/frontend-web/assets/images/output.png b/frontend-web/assets/images/output.png new file mode 100644 index 0000000..f32e613 Binary files /dev/null and b/frontend-web/assets/images/output.png differ diff --git a/frontend-web/assets/images/output24.png b/frontend-web/assets/images/output24.png new file mode 100644 index 0000000..32a4738 Binary files /dev/null and b/frontend-web/assets/images/output24.png differ diff --git a/frontend-web/assets/images/paste24.png b/frontend-web/assets/images/paste24.png new file mode 100644 index 0000000..850f984 Binary files /dev/null and b/frontend-web/assets/images/paste24.png differ diff --git a/frontend-web/assets/images/previous.png b/frontend-web/assets/images/previous.png new file mode 100644 index 0000000..22825f2 Binary files /dev/null and b/frontend-web/assets/images/previous.png differ diff --git a/frontend-web/assets/images/previous24.png b/frontend-web/assets/images/previous24.png new file mode 100644 index 0000000..2ebd207 Binary files /dev/null and b/frontend-web/assets/images/previous24.png differ diff --git a/frontend-web/assets/images/qrcode.svg b/frontend-web/assets/images/qrcode.svg new file mode 100644 index 0000000..feff057 --- /dev/null +++ b/frontend-web/assets/images/qrcode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/qrcode32.png b/frontend-web/assets/images/qrcode32.png new file mode 100644 index 0000000..7d2bf83 Binary files /dev/null and b/frontend-web/assets/images/qrcode32.png differ diff --git a/frontend-web/assets/images/qrcode400.png b/frontend-web/assets/images/qrcode400.png new file mode 100644 index 0000000..5e9bc6d Binary files /dev/null and b/frontend-web/assets/images/qrcode400.png differ diff --git a/frontend-web/assets/images/rect.svg b/frontend-web/assets/images/rect.svg new file mode 100644 index 0000000..26d1e33 --- /dev/null +++ b/frontend-web/assets/images/rect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/rect24.png b/frontend-web/assets/images/rect24.png new file mode 100644 index 0000000..f32a6a6 Binary files /dev/null and b/frontend-web/assets/images/rect24.png differ diff --git a/frontend-web/assets/images/rect32.png b/frontend-web/assets/images/rect32.png new file mode 100644 index 0000000..0f794f2 Binary files /dev/null and b/frontend-web/assets/images/rect32.png differ diff --git a/frontend-web/assets/images/right24.png b/frontend-web/assets/images/right24.png new file mode 100644 index 0000000..7cf3b16 Binary files /dev/null and b/frontend-web/assets/images/right24.png differ diff --git a/frontend-web/assets/images/rotate.png b/frontend-web/assets/images/rotate.png new file mode 100644 index 0000000..0e629a9 Binary files /dev/null and b/frontend-web/assets/images/rotate.png differ diff --git a/frontend-web/assets/images/rotate20.png b/frontend-web/assets/images/rotate20.png new file mode 100644 index 0000000..1f01dd4 Binary files /dev/null and b/frontend-web/assets/images/rotate20.png differ diff --git a/frontend-web/assets/images/round.svg b/frontend-web/assets/images/round.svg new file mode 100644 index 0000000..87f4c5b --- /dev/null +++ b/frontend-web/assets/images/round.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/save.png b/frontend-web/assets/images/save.png new file mode 100644 index 0000000..4aa5dcb Binary files /dev/null and b/frontend-web/assets/images/save.png differ diff --git a/frontend-web/assets/images/save24.png b/frontend-web/assets/images/save24.png new file mode 100644 index 0000000..2667ae7 Binary files /dev/null and b/frontend-web/assets/images/save24.png differ diff --git a/frontend-web/assets/images/text.svg b/frontend-web/assets/images/text.svg new file mode 100644 index 0000000..49f5015 --- /dev/null +++ b/frontend-web/assets/images/text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/text2.svg b/frontend-web/assets/images/text2.svg new file mode 100644 index 0000000..b28c264 --- /dev/null +++ b/frontend-web/assets/images/text2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/assets/images/text24.png b/frontend-web/assets/images/text24.png new file mode 100644 index 0000000..f172712 Binary files /dev/null and b/frontend-web/assets/images/text24.png differ diff --git a/frontend-web/assets/images/text3.svg b/frontend-web/assets/images/text3.svg new file mode 100644 index 0000000..dd94d60 --- /dev/null +++ b/frontend-web/assets/images/text3.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/frontend-web/assets/images/text32.png b/frontend-web/assets/images/text32.png new file mode 100644 index 0000000..2fea7fc Binary files /dev/null and b/frontend-web/assets/images/text32.png differ diff --git a/frontend-web/assets/images/top24.png b/frontend-web/assets/images/top24.png new file mode 100644 index 0000000..14d5197 Binary files /dev/null and b/frontend-web/assets/images/top24.png differ diff --git a/frontend-web/assets/images/underline24.png b/frontend-web/assets/images/underline24.png new file mode 100644 index 0000000..c4eef35 Binary files /dev/null and b/frontend-web/assets/images/underline24.png differ diff --git a/frontend-web/assets/images/unlock20.png b/frontend-web/assets/images/unlock20.png new file mode 100644 index 0000000..897bbce Binary files /dev/null and b/frontend-web/assets/images/unlock20.png differ diff --git a/frontend-web/assets/images/web/admin/audits.svg b/frontend-web/assets/images/web/admin/audits.svg new file mode 100644 index 0000000..996a98b --- /dev/null +++ b/frontend-web/assets/images/web/admin/audits.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/admin/dashboard.svg b/frontend-web/assets/images/web/admin/dashboard.svg new file mode 100644 index 0000000..537b80a --- /dev/null +++ b/frontend-web/assets/images/web/admin/dashboard.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/admin/empty.svg b/frontend-web/assets/images/web/admin/empty.svg new file mode 100644 index 0000000..058d4b3 --- /dev/null +++ b/frontend-web/assets/images/web/admin/empty.svg @@ -0,0 +1,7 @@ + diff --git a/frontend-web/assets/images/web/admin/orders.svg b/frontend-web/assets/images/web/admin/orders.svg new file mode 100644 index 0000000..b97d8bd --- /dev/null +++ b/frontend-web/assets/images/web/admin/orders.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/admin/payment.svg b/frontend-web/assets/images/web/admin/payment.svg new file mode 100644 index 0000000..9ebc5eb --- /dev/null +++ b/frontend-web/assets/images/web/admin/payment.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/admin/plans.svg b/frontend-web/assets/images/web/admin/plans.svg new file mode 100644 index 0000000..b2d7c25 --- /dev/null +++ b/frontend-web/assets/images/web/admin/plans.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/admin/settings.svg b/frontend-web/assets/images/web/admin/settings.svg new file mode 100644 index 0000000..0753519 --- /dev/null +++ b/frontend-web/assets/images/web/admin/settings.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/admin/users.svg b/frontend-web/assets/images/web/admin/users.svg new file mode 100644 index 0000000..36e1240 --- /dev/null +++ b/frontend-web/assets/images/web/admin/users.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/assets/images/web/empty-state.png b/frontend-web/assets/images/web/empty-state.png new file mode 100644 index 0000000..8b13764 Binary files /dev/null and b/frontend-web/assets/images/web/empty-state.png differ diff --git a/frontend-web/assets/images/web/hero-auth.svg b/frontend-web/assets/images/web/hero-auth.svg new file mode 100644 index 0000000..bb4c6ef --- /dev/null +++ b/frontend-web/assets/images/web/hero-auth.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend-web/assets/images/web/member-pro-badge.svg b/frontend-web/assets/images/web/member-pro-badge.svg new file mode 100644 index 0000000..1667897 --- /dev/null +++ b/frontend-web/assets/images/web/member-pro-badge.svg @@ -0,0 +1,6 @@ + + + + + PRO + diff --git a/frontend-web/js/auth-form.js b/frontend-web/js/auth-form.js new file mode 100644 index 0000000..d820089 --- /dev/null +++ b/frontend-web/js/auth-form.js @@ -0,0 +1,81 @@ +(function () { + 'use strict'; + + function initAuthForm(options) { + options = options || {}; + var formId = options.formId; + var errorId = options.errorId; + var passwordId = options.passwordId || 'password'; + var pwdToggleId = options.pwdToggleId || 'pwdToggle'; + var endpoint = options.endpoint || ''; + var failMsg = options.failMsg || '操作失败'; + var redirectFallback = options.redirectFallback || 'index.web.html'; + + if (options.redirectIfAuthed !== false && localStorage.getItem('soon_access')) { + var p = new URLSearchParams(location.search); + location.href = soonSafeRedirect(p.get('redirect'), redirectFallback); + return; + } + + if (options.linkId && options.linkHrefBase) { + var redirectQ = new URLSearchParams(location.search).get('redirect'); + if (redirectQ) { + var link = document.getElementById(options.linkId); + if (link) link.href = options.linkHrefBase + '?redirect=' + encodeURIComponent(redirectQ); + } + } + + var pwdToggle = document.getElementById(pwdToggleId); + if (pwdToggle) { + pwdToggle.onclick = function () { + var p = document.getElementById(passwordId); + var show = p.type === 'password'; + p.type = show ? 'text' : 'password'; + this.textContent = show ? '隐藏' : '显示'; + }; + } + + var form = document.getElementById(formId); + if (!form || !endpoint) return; + + form.onsubmit = function (e) { + e.preventDefault(); + var err = document.getElementById(errorId); + var pwd = document.getElementById(passwordId); + if (err) { + err.style.display = 'none'; + err.textContent = ''; + } + if (pwd) pwd.classList.remove('is-error'); + + var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '/api/v1'; + var params = new URLSearchParams(location.search); + var body = typeof options.getBody === 'function' ? options.getBody() : {}; + + fetch(base + endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).then(function (r) { return r.json(); }).then(function (j) { + if (j.ok) { + localStorage.setItem('soon_access', j.data.access_token); + localStorage.setItem('soon_refresh', j.data.refresh_token); + location.href = soonSafeRedirect(params.get('redirect'), redirectFallback); + } else { + if (err) { + err.textContent = j.message || failMsg; + err.style.display = 'block'; + } + if (pwd) pwd.classList.add('is-error'); + } + }).catch(function () { + if (err) { + err.textContent = '网络错误,请稍后重试'; + err.style.display = 'block'; + } + }); + }; + } + + window.soonInitAuthForm = initAuthForm; +})(); diff --git a/frontend-web/js/common/asset-base.js b/frontend-web/js/common/asset-base.js new file mode 100644 index 0000000..aeec558 --- /dev/null +++ b/frontend-web/js/common/asset-base.js @@ -0,0 +1,41 @@ +(function () { + 'use strict'; + + function computeAssetBase() { + if (typeof window === 'undefined' || !window.location) { + return 'assets/images/'; + } + var p = window.location.pathname || '/'; + if (p.indexOf('/pages/') !== -1 || /\.web\.html$/i.test(p) || /\/pages\/[^/]+\.html$/i.test(p)) { + return '../assets/images/'; + } + if (p.endsWith('/pages/') || p.endsWith('/pages')) { + return '../assets/images/'; + } + return 'assets/images/'; + } + + function computeJsBase() { + if (typeof window === 'undefined' || !window.location) { + return 'js/'; + } + var p = window.location.pathname || '/'; + if (p.indexOf('/pages/') !== -1 || /\.web\.html$/i.test(p) || /\/pages\/[^/]+\.html$/i.test(p)) { + return '../js/'; + } + return 'js/'; + } + + window.SOON_ASSET_BASE = computeAssetBase(); + window.SOON_JS_BASE = computeJsBase(); + + window.soonAsset = function (name) { + var n = String(name || '').replace(/^\/+/, ''); + return window.SOON_ASSET_BASE + n; + }; + + window.soonJs = function (rel) { + var n = String(rel || '').replace(/^\/+/, ''); + return window.SOON_JS_BASE + n; + }; +})(); diff --git a/frontend-web/js/common/auth-redirect.js b/frontend-web/js/common/auth-redirect.js new file mode 100644 index 0000000..3a6d62c --- /dev/null +++ b/frontend-web/js/common/auth-redirect.js @@ -0,0 +1,73 @@ +(function () { + 'use strict'; + + var ALLOWED = /^(index\.web\.html|member\.web\.html|login\.web\.html|register\.web\.html|admin\/(?:index|login)\.html|index\.html|login\.html|design[12]\.web\.html)(\?.*)?$/i; + + function soonEscapeHtml(s) { + if (s == null) return ''; + return String(s) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + } + + function soonSafeRedirect(urlParam, fallback) { + fallback = fallback || 'index.web.html'; + if (!urlParam || typeof urlParam !== 'string') return fallback; + var raw = urlParam.trim(); + if (!raw) return fallback; + try { + raw = decodeURIComponent(raw); + } catch (e) { + return fallback; + } + var lower = raw.toLowerCase(); + if (lower.indexOf('//') === 0 || lower.indexOf('http:') === 0 || lower.indexOf('https:') === 0 || + lower.indexOf('javascript:') === 0 || raw.indexOf('..') !== -1 || raw.indexOf('\\') !== -1) { + return fallback; + } + var pathOnly = raw.split(/[#?]/)[0]; + if (ALLOWED.test(pathOnly)) return raw; + try { + var resolved = new URL(raw, window.location.href); + if (resolved.origin !== window.location.origin) return fallback; + var path = resolved.pathname.replace(/^.*\/pages\//, '').replace(/^\//, ''); + if (!ALLOWED.test(path.split(/[#?]/)[0])) return fallback; + return path + resolved.search + resolved.hash; + } catch (e) { + return fallback; + } + } + + function soonSafeImageUrl(url, fallback) { + if (!url || typeof url !== 'string') return fallback || ''; + var raw = url.trim(); + if (!raw) return fallback || ''; + var lower = raw.toLowerCase(); + if (lower.indexOf('javascript:') === 0 || lower.indexOf('data:text/html') === 0 || + lower.indexOf('vbscript:') === 0 || lower.indexOf('//') === 0) { + return fallback || ''; + } + if (lower.indexOf('data:image/') === 0) return raw; + if (lower.indexOf('blob:') === 0) return raw; + if (lower.indexOf('https:') === 0) return raw; + if (lower.indexOf('http:') === 0) { + try { + var apiBase = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base); + if (apiBase && new URL(raw).origin === new URL(apiBase).origin) return raw; + } catch (e) { /* fall through */ } + } + if (raw.indexOf('../') === 0 || raw.indexOf('./') === 0 || raw.indexOf('/') === 0) { + try { + var resolved = new URL(raw, window.location.href); + if (resolved.origin === window.location.origin) return raw; + } catch (e) { /* fall through */ } + } + return fallback || ''; + } + + window.soonEscapeHtml = soonEscapeHtml; + window.soonSafeRedirect = soonSafeRedirect; + window.soonSafeImageUrl = soonSafeImageUrl; +})(); diff --git a/frontend-web/js/common/auth-session.js b/frontend-web/js/common/auth-session.js new file mode 100644 index 0000000..4187056 --- /dev/null +++ b/frontend-web/js/common/auth-session.js @@ -0,0 +1,107 @@ +(function () { + 'use strict'; + + var refreshPromise = null; + + function getAccessToken() { + try { return localStorage.getItem('soon_access') || ''; } catch (e) { return ''; } + } + + function getRefreshToken() { + try { return localStorage.getItem('soon_refresh') || ''; } catch (e) { return ''; } + } + + function clearSession() { + try { + localStorage.removeItem('soon_access'); + localStorage.removeItem('soon_refresh'); + } catch (e) { /* ignore */ } + } + + function getApiBase() { + var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base); + if (cfg) return String(cfg).replace(/\/+$/, ''); + if (typeof window !== 'undefined' && window.location) { + return window.location.origin + '/api/v1'; + } + return '/api/v1'; + } + + function redirectToLogin() { + var path = 'login.web.html'; + try { + var current = window.location.pathname.replace(/^.*\/pages\//, '').replace(/^\//, ''); + if (current && current.indexOf('login.web.html') !== 0) { + var redirect = typeof soonSafeRedirect === 'function' + ? soonSafeRedirect(current + window.location.search, 'index.web.html') + : 'index.web.html'; + path = 'login.web.html?redirect=' + encodeURIComponent(redirect); + } + } catch (e) { /* use default */ } + window.location.href = path; + } + + function refreshAccessToken() { + if (refreshPromise) return refreshPromise; + var refresh = getRefreshToken(); + if (!refresh) { + return Promise.reject(new Error('no_refresh_token')); + } + refreshPromise = fetch(getApiBase() + '/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: refresh }), + }).then(function (r) { + return r.json().then(function (j) { + if (!r.ok || !j.ok || !j.data || !j.data.access_token) { + throw new Error(j.message || 'refresh_failed'); + } + localStorage.setItem('soon_access', j.data.access_token); + if (j.data.refresh_token) { + localStorage.setItem('soon_refresh', j.data.refresh_token); + } + return j.data.access_token; + }); + }).finally(function () { + refreshPromise = null; + }); + return refreshPromise; + } + + function soonAuthedFetch(url, opts) { + opts = opts || {}; + var headers = Object.assign({}, opts.headers || {}); + var token = getAccessToken(); + if (token) { + headers['Authorization'] = 'Bearer ' + token; + } else { + delete headers['Authorization']; + delete headers['authorization']; + } + opts.headers = headers; + + var fullUrl = url; + if (url.indexOf('http') !== 0) { + fullUrl = getApiBase() + '/' + String(url).replace(/^\/+/, ''); + } + + return fetch(fullUrl, opts).then(function (response) { + if (response.status !== 401) return response; + return refreshAccessToken().then(function (newToken) { + var retryHeaders = Object.assign({}, opts.headers || {}); + retryHeaders['Authorization'] = 'Bearer ' + newToken; + var retryOpts = Object.assign({}, opts, { headers: retryHeaders }); + return fetch(fullUrl, retryOpts); + }).catch(function () { + clearSession(); + redirectToLogin(); + return response; + }); + }); + } + + window.soonAuthedFetch = soonAuthedFetch; + window.soonClearAuthSession = clearSession; + window.soonGetAccessToken = getAccessToken; + window.soonRedirectToLogin = redirectToLogin; +})(); diff --git a/frontend-web/js/common/cloud-files.js b/frontend-web/js/common/cloud-files.js new file mode 100644 index 0000000..57e9f66 --- /dev/null +++ b/frontend-web/js/common/cloud-files.js @@ -0,0 +1,312 @@ +(function () { + 'use strict'; + + var FILE_PREFIX = 'soondesign_file:'; + + function soonIsWebPortal() { + return typeof window !== 'undefined' && window.platformBridge && !window.fs; + } + + function soonGetAccessToken() { + if (typeof window.soonGetAccessToken === 'function') { + return window.soonGetAccessToken(); + } + try { return localStorage.getItem('soon_access') || ''; } catch (e) { return ''; } + } + + function soonParseFileKey(key) { + if (!key || typeof key !== 'string') return null; + var m = key.match(/^soondesign_file:(\d+)(?::v(\d+))?$/); + if (!m) return null; + return { id: parseInt(m[1], 10), version: m[2] ? parseInt(m[2], 10) : null }; + } + + function soonMakeFileKey(id, version) { + return FILE_PREFIX + id + ':v' + version; + } + + function soonNormalizeSoonName(pathOrName) { + var n = String(pathOrName || 'design.soon'); + if (n.indexOf(FILE_PREFIX) === 0) { + var meta = window._soonFileMeta; + return (meta && meta.name) ? meta.name : 'design.soon'; + } + if (n.indexOf('soondesign_session:') === 0) { + n = n.replace(/^soondesign_session:/, ''); + } + n = n.split(/[/\\]/).pop(); + if (!/\.soon$/i.test(n)) n = (n.replace(/\.soon$/i, '') || 'design') + '.soon'; + return n; + } + + function soonApplyCloudMeta(data) { + if (!data || !data.id) return null; + window._soonFileMeta = { id: data.id, version: data.version, name: data.name }; + return soonMakeFileKey(data.id, data.version); + } + + function soonBindFileMeta(fileKey, opts) { + if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) return; + var parsed = soonParseFileKey(fileKey); + if (!parsed) return; + var prev = window._soonFileMeta || {}; + window._soonFileMeta = { + id: parsed.id, + version: parsed.version != null ? parsed.version : prev.version, + name: (opts && opts.name) || (prev.id === parsed.id ? prev.name : '') || '' + }; + } + + function soonFormatOpenTitle(name) { + if (!name) return ''; + if (typeof window.soonDisplayFileName === 'function') return window.soonDisplayFileName(name); + return String(name).split(/[/\\]/).pop(); + } + + var _membership = { + loaded: false, + isMember: false, + name: '免费版', + tier: 'free', + expiresAt: null, + loading: null, + }; + + function soonApiBase() { + var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base); + if (cfg) return String(cfg).replace(/\/+$/, ''); + if (typeof window !== 'undefined' && window.location) { + return window.location.origin + '/api/v1'; + } + return '/api/v1'; + } + + function formatExpireDate(iso) { + if (!iso) return ''; + var d = new Date(String(iso).replace(' ', 'T')); + if (isNaN(d.getTime())) return ''; + var y = d.getFullYear(); + var mo = String(d.getMonth() + 1); + var day = String(d.getDate()); + if (mo.length < 2) mo = '0' + mo; + if (day.length < 2) day = '0' + day; + return y + '-' + mo + '-' + day; + } + + function soonApplyMembership(data) { + var m = data || {}; + var sub = m.subscription || {}; + var isMember = m.is_member === true + || m.tier === 'member' + || m.tier === 'pro' + || (sub.status && sub.status !== 'free' && sub.status !== 'expired'); + _membership = { + loaded: true, + isMember: !!isMember, + name: m.name || (isMember ? '订阅版' : '免费版'), + tier: m.tier || (isMember ? 'member' : 'free'), + expiresAt: sub.expires_at || null, + loading: null, + }; + if (typeof window.soonRefreshPortalIdentity === 'function') { + window.soonRefreshPortalIdentity(); + } + return _membership; + } + + function soonRefreshPortalIdentity() { + var el = document.getElementById('auth_identity'); + if (!el) return; + if (!soonGetAccessToken()) { + el.style.display = 'none'; + el.textContent = ''; + return; + } + if (!_membership.loaded) { + soonLoadMembership(false).then(function () { + soonRefreshPortalIdentity(); + }); + return; + } + var text; + var cls = 'soon-portal-identity'; + if (_membership.isMember) { + cls += ' soon-portal-identity--member'; + text = _membership.name || '订阅版'; + if (_membership.expiresAt) { + text += ' · 至 ' + formatExpireDate(_membership.expiresAt); + } + } else { + cls += ' soon-portal-identity--free'; + text = _membership.name || '免费版'; + } + el.className = cls; + el.textContent = text; + el.style.display = 'inline-flex'; + var account = document.getElementById('auth_account'); + if (account) account.style.display = 'inline-flex'; + } + + function soonLoadMembership(force) { + if (_membership.loaded && !force) return Promise.resolve(_membership); + if (_membership.loading && !force) return _membership.loading; + if (!soonGetAccessToken()) { + soonApplyMembership(null); + return Promise.resolve(_membership); + } + var url = soonApiBase() + '/plans/me'; + var fetchFn = typeof window.soonAuthedFetch === 'function' + ? window.soonAuthedFetch(url, { headers: { Accept: 'application/json' } }) + : fetch(url, { headers: { Authorization: 'Bearer ' + soonGetAccessToken(), Accept: 'application/json' } }); + _membership.loading = fetchFn + .then(function (r) { return r.json(); }) + .then(function (j) { + if (j && j.ok && j.data) { + soonApplyMembership(j.data.membership || j.data); + } else { + soonApplyMembership(null); + } + return _membership; + }) + .catch(function () { + soonApplyMembership(null); + return _membership; + }) + .finally(function () { + _membership.loading = null; + }); + return _membership.loading; + } + + function soonIsMember() { + return !!_membership.isMember; + } + + function soonMemberPageUrl() { + return 'member.web.html'; + } + + function soonRequireMember(actionLabel) { + if (!soonGetAccessToken()) return soonRequireLogin(actionLabel); + if (soonIsMember()) return true; + if (typeof window.soonShowSubscribeGate === 'function') { + window.soonShowSubscribeGate({ action: actionLabel || '使用' }); + } else { + soonToast('订阅后可' + (actionLabel || '使用'), 'warn'); + } + return false; + } + + function soonGuardCloudSave() { + if (!soonIsWebPortal()) return true; + return soonRequireMember('保存'); + } + + function soonGuardMemberPreview() { + if (!soonIsWebPortal()) return true; + return soonRequireMember('预览效果'); + } + + function soonGuardMemberExport() { + if (!soonIsWebPortal()) return true; + return soonRequireMember('导出'); + } + + function soonToast(message, type) { + if (typeof window.soonToast === 'function') window.soonToast(message, type); + } + + function soonRequireLogin(actionLabel) { + if (soonGetAccessToken()) return true; + var label = actionLabel || '继续'; + soonToast('请先登录后再' + label, 'warn'); + if (typeof window.soonRedirectToLogin === 'function') { + setTimeout(function () { window.soonRedirectToLogin(); }, 600); + } else if (typeof window.redirectToLogin === 'function') { + setTimeout(function () { window.redirectToLogin(); }, 600); + } else { + window.location.href = 'login.web.html'; + } + return false; + } + + function soonParseApiError(response, bodyText) { + var msg = '操作失败,请稍后重试'; + var code = ''; + try { + var j = bodyText ? JSON.parse(bodyText) : null; + if (j && j.message) msg = j.message; + if (j && j.error) code = j.error; + } catch (e) { /* ignore */ } + if (response && response.status === 401) { + return { status: 401, message: '请先登录', code: code || 'unauthorized' }; + } + if (response && response.status === 409) { + return { status: 409, message: '文件已被其他端修改,请刷新后重试', code: code || 'conflict' }; + } + if (response && response.status === 403 && (code === 'membership_required' || msg.indexOf('订阅') >= 0 || msg.indexOf('会员') >= 0)) { + return { status: 403, message: msg || '此功能需要订阅后使用', code: code || 'membership_required' }; + } + if (response && response.status === 413) { + if (code === 'file_limit_exceeded') { + return { status: 413, message: msg || '文件数量已达上限,请清理文件或续订', code: code }; + } + return { status: 413, message: msg || '存储空间不足,请清理文件或续订', code: code || 'quota_exceeded' }; + } + if (response && response.status === 404) { + return { status: 404, message: '文件不存在', code: code || 'not_found' }; + } + return { status: response ? response.status : 0, message: msg, code: code }; + } + + function soonShowApiError(info) { + if (!info) return; + var kind = 'error'; + if (info.status === 401) kind = 'warn'; + else if (info.status === 409) kind = 'warn'; + else if (info.status === 413) kind = 'warn'; + else if (info.status === 403 && info.code === 'membership_required') kind = 'warn'; + soonToast(info.message, kind); + if (info.status === 401) { + soonRequireLogin('操作'); + } else if (info.status === 403 && info.code === 'membership_required') { + if (typeof window.soonShowSubscribeGate === 'function') { + window.soonShowSubscribeGate({ action: '使用' }); + } + } + } + + function soonDisplayFileName(pathOrKey) { + if (!pathOrKey) return ''; + if (pathOrKey.indexOf(FILE_PREFIX) === 0) { + var meta = window._soonFileMeta; + if (meta && meta.name) return meta.name; + var parsed = soonParseFileKey(pathOrKey); + return parsed ? ('文件 #' + parsed.id) : pathOrKey; + } + if (pathOrKey.indexOf('soondesign_session:') === 0) { + return pathOrKey.replace(/^soondesign_session:/, ''); + } + return String(pathOrKey).split(/[/\\]/).pop(); + } + + window.soonIsWebPortal = soonIsWebPortal; + window.soonParseFileKey = soonParseFileKey; + window.soonMakeFileKey = soonMakeFileKey; + window.soonNormalizeSoonName = soonNormalizeSoonName; + window.soonApplyCloudMeta = soonApplyCloudMeta; + window.soonBindFileMeta = soonBindFileMeta; + window.soonFormatOpenTitle = soonFormatOpenTitle; + window.soonGuardCloudSave = soonGuardCloudSave; + window.soonGuardMemberPreview = soonGuardMemberPreview; + window.soonGuardMemberExport = soonGuardMemberExport; + window.soonLoadMembership = soonLoadMembership; + window.soonApplyMembership = soonApplyMembership; + window.soonRefreshPortalIdentity = soonRefreshPortalIdentity; + window.soonIsMember = soonIsMember; + window.soonRequireMember = soonRequireMember; + window.soonRequireLogin = soonRequireLogin; + window.soonParseApiError = soonParseApiError; + window.soonShowApiError = soonShowApiError; + window.soonDisplayFileName = soonDisplayFileName; +})(); diff --git a/frontend-web/js/common/fabric-ext.js b/frontend-web/js/common/fabric-ext.js new file mode 100644 index 0000000..1a17cde --- /dev/null +++ b/frontend-web/js/common/fabric-ext.js @@ -0,0 +1,164 @@ +// Fabric.js 扩展类 + +(function (fabric) { + fabric.CurvedText = fabric.util.createClass(fabric.Object, { + type: 'curved-text', + diameter: 0, + kerning: 0, + text: '', + flipped: false, + fill: '#000', + fontFamily: 'Times New Roman', + fontSize: 24, // in px + fontWeight: 'normal', + fontStyle: '', // "normal", "italic" or "oblique". + cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), + strokeStyle: null, + strokeWidth: 0, + initialize: function (text, options) { + options || (options = {}); + if (typeof text === 'object' && text) { + for (let key in text) { + this[key] = text[key] + } + } else { + this.text = text; + } + this.callSuper('initialize', options); + this.set('lockUniScaling', true); + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + }, + _getFontDeclaration: function () { + return [ + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + cropCanvas: function (canvas) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + pix = { x: [], y: [] }, n, + imageData = ctx.getImageData(0, 0, w, h), + fn = function (a, b) { return a - b }; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + if (imageData.data[((y * w + x) * 4) + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // 如果没有找到任何非透明像素,直接返回,不进行裁剪 + if (pix.x.length === 0 || pix.y.length === 0) { + return; + } + pix.x.sort(fn); + pix.y.sort(fn); + n = pix.x.length - 1; + w = pix.x[n] - pix.x[0]; + h = pix.y[n] - pix.y[0]; + // 确保宽度和高度是有效的正数 + if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { + return; + } + var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); + canvas.width = w; + canvas.height = h; + ctx.putImageData(cut, 0, 0); + }, + getCircularText: function () { + var text = this.text, + diameter = this.diameter, + flipped = this.flipped, + kerning = this.kerning, + fill = this.fill, + inwardFacing = true, + startAngle = 0, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'), + cw, // character-width + x, // iterator + clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise + if (flipped) { + // startAngle = 180; + inwardFacing = false; + } + startAngle *= Math.PI / 180; // convert to radians + var d = document.createElement('div'); + d.style.fontFamily = this.fontFamily; + d.style.fontSize = this.fontSize + 'px'; + d.style.fontWeight = this.fontWeight; + d.style.fontStyle = this.fontStyle; + d.textContent = text; + document.body.appendChild(d); + var textHeight = d.offsetHeight; + document.body.removeChild(d); + canvas.width = canvas.height = diameter; + ctx.font = this._getFontDeclaration(); + if (inwardFacing) { text = text.split('').reverse().join('') }; + ctx.translate(diameter / 2, diameter / 2); // Move to center + startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward + ctx.textBaseline = 'middle'; // Ensure we draw in exact center + ctx.textAlign = 'center'; // Ensure we draw in exact center + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; + startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; + } + ctx.rotate(startAngle); + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; // half letter + ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); + if (this.strokeStyle && this.strokeWidth) { + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.miterLimit = 2; + ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + } + ctx.fillStyle = fill; + ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter + } + return canvas; + }, + _set: function (key, value) { + switch (key) { + case 'scaleX': + this.fontSize *= value; + this.diameter *= value; + this.width *= value; + this.scaleX = 1; + if (this.width < 1) { this.width = 1; } + break; + case 'scaleY': + this.height *= value; + this.scaleY = 1; + if (this.height < 1) { this.height = 1; } + break; + default: + this.callSuper('_set', key, value); + break; + } + }, + _render: function (ctx) { + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); + this.setCoords(); + }, + toObject: function (propertiesToInclude) { + return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); + } + }); + fabric.CurvedText.fromObject = function (object, callback, forceAsync) { + return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); + }; +})(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); + diff --git a/frontend-web/js/common/language.js b/frontend-web/js/common/language.js new file mode 100644 index 0000000..dc7bf1c --- /dev/null +++ b/frontend-web/js/common/language.js @@ -0,0 +1,56 @@ +// 多语言支持模块 + +/** + * 多语言切换 + */ +function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)); + }); + + $("[language='t']").each(function (i) { + $(this).attr("title", $(this).attr(lan)); + }); +} + +/** + * 多语言字符串获取 + * @param {string} str - 字符串键 + * @param {string} s_lan - 当前语言 ('zh', 'ozh', 'en') + */ +function language_str(str, s_lan) { + let t = { + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, + "save": { zh: "保存", ozh: "保存", en: "Save" }, + "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, + "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, + "display": { zh: "预览", ozh: "預覽", en: "Display" }, + "output": { zh: "导出", ozh: "導出", en: "Export" }, + "bg": { zh: "背景", ozh: "背景", en: "Background" }, + "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, + "img": { zh: "圖片", ozh: "圖片", en: "Image" }, + "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, + "text": { zh: "文本", ozh: "文本", en: "Text" }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, + "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, + "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, + "line": { zh: "直線", ozh: "直線", en: "Line" }, + "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, + "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, + "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, + "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, + "vers": { zh: "版本", ozh: "版本", en: "Version" }, + "comf": { zh: "确认", ozh: "確認", en: "Select" }, + "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + }; + return t[str] ? t[str][s_lan] : str; +} + +module.exports = { + langua_ge, + language_str +}; + diff --git a/frontend-web/js/common/member-pay-core.js b/frontend-web/js/common/member-pay-core.js new file mode 100644 index 0000000..37a86d0 --- /dev/null +++ b/frontend-web/js/common/member-pay-core.js @@ -0,0 +1,464 @@ +(function () { + 'use strict'; + + var POLL_TIMEOUT_MS = 5 * 60 * 1000; + var PAY_STEP_LABELS = ['选择支付', '扫码付款', '订阅生效']; + + function esc(s) { + if (s == null) return ''; + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function apiBase() { + var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base); + if (cfg) return String(cfg).replace(/\/+$/, ''); + if (typeof window !== 'undefined' && window.location) { + return window.location.origin + '/api/v1'; + } + return '/api/v1'; + } + + function apiFetch(url, opts) { + if (typeof window.soonAuthedFetch === 'function') return window.soonAuthedFetch(url, opts || {}); + opts = opts || {}; + var token = localStorage.getItem('soon_access') || ''; + opts.headers = Object.assign({}, opts.headers || {}); + if (token) opts.headers.Authorization = 'Bearer ' + token; + return fetch(url, opts); + } + + function parseApiJson(r) { + return r.text().then(function (text) { + try { + return JSON.parse(text); + } catch (e) { + return { ok: false, message: '服务暂时不可用,请稍后重试' }; + } + }); + } + + function apiGet(p) { + return apiFetch(apiBase() + p).then(function (r) { + if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' }; + return parseApiJson(r); + }); + } + + function apiPost(p, body) { + return apiFetch(apiBase() + p, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}), + }).then(function (r) { + if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' }; + return parseApiJson(r); + }); + } + + function isSafeAlipayGateway(url) { + if (!url || typeof url !== 'string') return false; + try { + var u = new URL(url); + return u.protocol === 'https:' + && (u.hostname === 'openapi.alipay.com' || u.hostname === 'openapi.alipaydev.com'); + } catch (e) { + return false; + } + } + + function submitAlipayForm(payForm) { + if (!payForm || !payForm.gateway || !payForm.params) return false; + if (!isSafeAlipayGateway(payForm.gateway)) return false; + var form = document.createElement('form'); + form.method = 'POST'; + form.action = payForm.gateway; + form.acceptCharset = 'UTF-8'; + form.style.display = 'none'; + Object.keys(payForm.params).forEach(function (key) { + var val = payForm.params[key]; + if (val == null || val === '') return; + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = key; + input.value = String(val); + form.appendChild(input); + }); + document.body.appendChild(form); + form.submit(); + return true; + } + + function payStepLabel(step, channel) { + if (step === 2 && channel === 'alipay') return '网页支付'; + if (step === 2 && channel === 'wechat') return '扫码付款'; + return PAY_STEP_LABELS[step - 1]; + } + + function payStepperHtml(activeStep, channel) { + var parts = []; + for (var i = 1; i <= 3; i++) { + if (i > 1) { + var bridgeCls = 'soon-pay-stepper__bridge'; + if (i - 1 < activeStep) bridgeCls += ' is-done'; + parts.push(''); + } + var cls = 'soon-pay-stepper__item'; + if (i < activeStep) cls += ' is-done'; + else if (i === activeStep) cls += ' is-active'; + parts.push('
  • ' + + '' + (i < activeStep ? '✓' : String(i)) + '' + + '' + payStepLabel(i, channel) + '
  • '); + } + return '
      ' + parts.join('') + '
    '; + } + + function updatePayStep(root, step, channel) { + if (!root) return; + var html = payStepperHtml(step, channel || 'wechat'); + var old = root.querySelector('#payStepper'); + if (old) old.outerHTML = html; + } + + function payEmptyStateHtml() { + return '
    ' + + '' + + '

    等待生成付款码

    ' + + '

    确认支付方式后,将在此显示二维码或跳转链接

    '; + } + + function renderPayPayload(data, qrEl, orderNoEl) { + if (orderNoEl && data.order_no) { + orderNoEl.textContent = '订单号 ' + data.order_no; + orderNoEl.style.display = ''; + } + if (data.pay_form && data.pay_form.gateway) { + if (!isSafeAlipayGateway(data.pay_form.gateway)) return '支付网关无效'; + sessionStorage.setItem('soon_pay_return', '1'); + var payForm = data.pay_form; + qrEl.innerHTML = + '
    ' + + '
    ' + + '

    支付宝电脑网站支付

    ' + + '

    点击下方按钮跳转至支付宝收银台,支付完成后将自动返回

    ' + + '
    '; + var alipayBtn = qrEl.querySelector('#goAlipay'); + if (alipayBtn) { + alipayBtn.onclick = function () { + if (!submitAlipayForm(payForm)) { + if (typeof window.soonToast === 'function') { + window.soonToast('无法跳转支付宝,请检查支付配置', 'warn'); + } + } + }; + } + return ''; + } + if (data.qrcode) { + qrEl.innerHTML = + '
    ' + + '
    ' + + '

    微信扫码支付

    ' + + '
    ' + + '

    请使用微信扫一扫,有效期约 2 小时

    '; + if (typeof QRCode !== 'undefined') { + new QRCode(qrEl.querySelector('#payQrCanvas'), { text: data.qrcode, width: 188, height: 188 }); + } + return ''; + } + return '未获取到支付信息'; + } + + function monthlyPlan(items) { + var found = null; + (items || []).forEach(function (p) { + if (p.code === 'member_monthly' || p.code === 'pro_monthly') found = p; + }); + return found; + } + + function billingSavePercent(items, plan) { + var monthly = monthlyPlan(items); + if (!monthly || monthly.price_cents <= 0 || !plan || plan.duration_days <= 30) return null; + var months = plan.duration_days / 30; + var full = monthly.price_cents * months; + if (full <= plan.price_cents) return null; + return Math.round((1 - plan.price_cents / full) * 100); + } + + function paySheetHtml(opts) { + opts = opts || {}; + var resumeOrderNo = opts.orderNo || null; + var channel = opts.channel || 'wechat'; + var embedded = opts.variant === 'embedded'; + var backBtn = !embedded && opts.showBack + ? '' + : ''; + var headBlock = embedded ? '' : + '
    ' + + '
    ' + + '' + (resumeOrderNo ? '继续支付' : '确认订阅') + '' + + '

    ' + esc(opts.planName || '') + '

    ' + + '
    ' + + '
    ' + + '¥' + + '' + esc(opts.priceDisplay || '') + '
    '; + var orderInBody = embedded + ? '' + : ''; + return '
    ' + + (backBtn ? '
    ' + backBtn + '
    ' : '') + + headBlock + + '
    ' + + orderInBody + + payStepperHtml(1, channel) + + '
    ' + + '

    支付方式

    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + payEmptyStateHtml() + '
    ' + + '
    ' + + '

    ' + + '' + + '' + + '
    '; + } + + function bindPaySheet(sheetRoot, opts) { + opts = opts || {}; + var planId = opts.planId; + var resumeOrderNo = opts.orderNo || null; + var channel = opts.channel || 'wechat'; + var onPaid = typeof opts.onPaid === 'function' ? opts.onPaid : function () {}; + var pollTimer = null; + var pollStartedAt = 0; + var orderNo = resumeOrderNo; + var paymentReady = false; + + if (!sheetRoot) return { destroy: function () {} }; + + var manualBtn = sheetRoot.querySelector('#payManualRefresh'); + var statusEl = sheetRoot.querySelector('#payStatus'); + var confirmBtn = sheetRoot.querySelector('#payConfirm'); + var qrEl = sheetRoot.querySelector('#payQr'); + var orderNoEl = sheetRoot.querySelector('#payOrderNo'); + var backBtn = sheetRoot.querySelector('#payBack'); + + function setStatus(text, type) { + if (!statusEl) return; + statusEl.textContent = text || ''; + statusEl.className = 'soon-pay-status' + (type ? ' soon-pay-status--' + type : ''); + statusEl.style.display = text ? 'block' : 'none'; + } + + function setStep(step) { + updatePayStep(sheetRoot, step, channel); + sheetRoot.classList.toggle('soon-pay-sheet--checkout', step >= 2); + var chSec = sheetRoot.querySelector('#payChannels'); + if (chSec) chSec.style.display = step >= 2 ? 'none' : ''; + } + + function stopPoll() { + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + } + + function startPoll() { + stopPoll(); + pollStartedAt = Date.now(); + pollTimer = setInterval(function () { + if (Date.now() - pollStartedAt > POLL_TIMEOUT_MS) { + stopPoll(); + setStatus('自动检测超时,如已支付请点击下方按钮', 'err'); + return; + } + checkOrderStatus(); + }, 3000); + } + + function handlePaid() { + stopPoll(); + setStep(3); + setStatus('支付成功,正在更新订阅…', 'ok'); + setTimeout(onPaid, 600); + } + + function checkOrderStatus() { + if (!orderNo) return; + apiGet('/pay/orders/' + encodeURIComponent(orderNo)).then(function (st) { + if (st.ok && st.data && st.data.status === 'paid') handlePaid(); + }); + } + + if (manualBtn) manualBtn.onclick = checkOrderStatus; + + function resetPaymentView() { + paymentReady = false; + stopPoll(); + setStep(1); + if (manualBtn) manualBtn.style.display = 'none'; + if (confirmBtn) confirmBtn.style.display = ''; + qrEl.innerHTML = payEmptyStateHtml(); + if (orderNoEl && !resumeOrderNo) { + orderNoEl.textContent = ''; + orderNoEl.style.display = 'none'; + } + setStatus(''); + } + + function applyCheckout(data) { + orderNo = data.order_no; + var err = renderPayPayload(data, qrEl, orderNoEl); + if (err) { + setStatus(err, 'err'); + return; + } + paymentReady = true; + setStep(2); + if (confirmBtn) confirmBtn.style.display = 'none'; + if (manualBtn) manualBtn.style.display = ''; + setStatus('等待支付结果,完成后将自动刷新', 'ok'); + startPoll(); + } + + function submitCheckout() { + if (confirmBtn) confirmBtn.disabled = true; + setStatus('正在生成付款信息…'); + qrEl.innerHTML = '
    ' + + '

    正在连接支付渠道…

    '; + var req; + if (resumeOrderNo || orderNo) { + var existingNo = orderNo || resumeOrderNo; + req = apiPost('/pay/orders/' + encodeURIComponent(existingNo) + '/checkout', { channel: channel }); + } else { + req = apiPost('/pay/orders', { plan_id: planId, channel: channel }); + } + req.then(function (j) { + if (confirmBtn) confirmBtn.disabled = false; + if (!j.ok) { + setStatus(j.message || '下单失败', 'err'); + qrEl.innerHTML = payEmptyStateHtml(); + return; + } + applyCheckout(j.data); + }).catch(function () { + if (confirmBtn) confirmBtn.disabled = false; + setStatus('网络错误,请重试', 'err'); + qrEl.innerHTML = payEmptyStateHtml(); + }); + } + + sheetRoot.querySelectorAll('.soon-pay-channel').forEach(function (el) { + el.onclick = function () { + if (el.classList.contains('is-active')) return; + sheetRoot.querySelectorAll('.soon-pay-channel').forEach(function (x) { x.classList.remove('is-active'); }); + el.classList.add('is-active'); + channel = el.dataset.ch; + if (paymentReady) resetPaymentView(); + }; + }); + + if (confirmBtn) confirmBtn.onclick = submitCheckout; + if (backBtn && typeof opts.onBack === 'function') backBtn.onclick = opts.onBack; + + function destroy() { + stopPoll(); + paymentReady = false; + orderNo = resumeOrderNo; + } + + return { + destroy: destroy, + reset: resetPaymentView, + }; + } + + function openPayModal(opts) { + opts = opts || {}; + if (typeof layer === 'undefined' || !layer.open) return null; + var html = paySheetHtml({ + planName: opts.planName, + priceDisplay: opts.priceDisplay, + orderNo: opts.orderNo, + channel: opts.channel || 'wechat', + }); + var payCtrl = null; + var idx = layer.open({ + type: 1, + skin: 'soon-layer', + title: false, + closeBtn: 1, + shadeClose: !!opts.shadeClose, + area: ['480px'], + content: html, + success: function (layero) { + var layerEl = layero && layero[0] ? layero[0] : layero; + if (layerEl && layerEl.classList) { + layerEl.classList.add('soon-layer--pay'); + var layerContent = layerEl.querySelector('.layui-layer-content'); + if (layerContent) { + layerContent.style.overflow = 'visible'; + layerContent.style.maxHeight = 'none'; + } + } + var sheet = layerEl.querySelector('.soon-pay-sheet'); + payCtrl = bindPaySheet(sheet, { + planId: opts.planId, + planName: opts.planName, + priceDisplay: opts.priceDisplay, + orderNo: opts.orderNo, + channel: opts.channel || 'wechat', + onPaid: function () { + layer.close(idx); + if (typeof opts.onPaid === 'function') opts.onPaid(); + }, + onBack: opts.onBack, + }); + }, + end: function () { + if (payCtrl) payCtrl.destroy(); + if (typeof opts.onClose === 'function') opts.onClose(); + }, + }); + return idx; + } + + window.SoonMemberPay = { + apiBase: apiBase, + apiGet: apiGet, + apiPost: apiPost, + esc: esc, + monthlyPlan: monthlyPlan, + billingSavePercent: billingSavePercent, + paySheetHtml: paySheetHtml, + bindPaySheet: bindPaySheet, + openPayModal: openPayModal, + payEmptyStateHtml: payEmptyStateHtml, + renderPayPayload: renderPayPayload, + submitAlipayForm: submitAlipayForm, + POLL_TIMEOUT_MS: POLL_TIMEOUT_MS, + }; +})(); diff --git a/frontend-web/js/common/member-plan-core.js b/frontend-web/js/common/member-plan-core.js new file mode 100644 index 0000000..447bb1c --- /dev/null +++ b/frontend-web/js/common/member-plan-core.js @@ -0,0 +1,104 @@ +(function () { + 'use strict'; + + var CHECK_SVG = '' + + ''; + + function esc(s) { + if (window.SoonMemberPay && window.SoonMemberPay.esc) return window.SoonMemberPay.esc(s); + if (s == null) return ''; + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function isPlanFeatureLine(line) { + if (!line) return false; + return line.indexOf('到期') < 0 + && line.indexOf('订阅有效期') < 0 + && line.indexOf('订阅周期') < 0 + && line.indexOf('权益一致') < 0; + } + + function featureItem(text) { + return '
  • ' + CHECK_SVG + '' + esc(text) + '
  • '; + } + + function billingSavePercent(items, plan) { + if (window.SoonMemberPay && window.SoonMemberPay.billingSavePercent) { + return window.SoonMemberPay.billingSavePercent(items, plan); + } + return null; + } + + function planCardHtml(plan, ctx) { + ctx = ctx || {}; + var items = ctx.allPlans || []; + var mode = ctx.mode || 'page'; + var isMember = !!ctx.isMember; + var currentCode = ctx.currentCode || ''; + var selectedId = ctx.selectedId; + + var feat = plan.is_recommended ? ' soon-plan-card--featured' : ''; + var cur = plan.code === currentCode ? ' soon-plan-card--current' : ''; + var isGate = mode === 'gate'; + var isPicker = mode === 'picker' || isGate; + var picker = isPicker ? ' soon-plan-card--picker' : ''; + if (isGate) picker += ' soon-plan-card--gate'; + var selected = isPicker && plan.id === selectedId ? ' soon-plan-card--selected' : ''; + + var ribbon = plan.is_recommended + ? '推荐' + : (plan.code === currentCode ? '当前' : ''); + var savePct = billingSavePercent(items, plan); + if (savePct) ribbon += '省 ' + savePct + '%'; + var badgeCls = ribbon ? ' soon-plan-card--badged' : ''; + if (savePct) badgeCls += ' soon-plan-card--save'; + + var period = plan.duration_days > 0 + ? '/' + esc(plan.period_label || plan.duration_days + '天') + '' + : ''; + var features = (plan.features || []).filter(isPlanFeatureLine).map(featureItem).join(''); + var desc = plan.description || ''; + var price = esc(plan.price_display || (plan.price_cents / 100).toFixed(2)); + + var inner = + ribbon + + '
    ' + + '
    ' + esc(plan.name) + '
    ' + + (desc ? '

    ' + esc(desc) + '

    ' : '') + + '
    ' + + '¥' + + '' + price + '' + period + '
    '; + if (!isGate) { + inner += '
    ' + + '
      ' + features + '
    '; + } + + if (isPicker) { + return ''; + } + + var cta = ''; + return '
    ' + inner + + '
    ' + cta + '
    '; + } + + function plansGridHtml(plans, ctx) { + return (plans || []).map(function (p) { + return planCardHtml(p, ctx); + }).join(''); + } + + window.SoonMemberPlan = { + isPlanFeatureLine: isPlanFeatureLine, + planCardHtml: planCardHtml, + plansGridHtml: plansGridHtml, + }; +})(); diff --git a/frontend-web/js/common/member-subscribe.js b/frontend-web/js/common/member-subscribe.js new file mode 100644 index 0000000..76a3a1f --- /dev/null +++ b/frontend-web/js/common/member-subscribe.js @@ -0,0 +1,306 @@ +(function () { + 'use strict'; + + var GATE_FEATURES = ['高清预览', '成品打印', '云端保存', '导出设计文件']; + var _gate = { index: null, payIndex: null, plans: [], selectedId: null, gateOpts: null }; + + var pay = window.SoonMemberPay; + var plansUi = window.SoonMemberPlan; + + function esc(s) { + if (pay && pay.esc) return pay.esc(s); + if (s == null) return ''; + return String(s).replace(/&/g, '&').replace(//g, '>'); + } + + function toast(msg, type) { + if (typeof window.soonToast === 'function') window.soonToast(msg, type); + } + + function emptyGate() { + return { index: null, payIndex: null, plans: [], selectedId: null, gateOpts: null }; + } + + function ensureLayer(cb) { + if (typeof layer !== 'undefined' && layer.open) { + if (layer.config) layer.config({ skin: 'soon-layer' }); + cb(); + return; + } + if (typeof layui !== 'undefined') { + layui.use(['layer'], function () { + window.layer = layui.layer; + layer.config({ skin: 'soon-layer' }); + cb(); + }); + return; + } + toast('订阅功能暂不可用,请刷新页面后重试', 'warn'); + } + + function actionCopy(actionLabel) { + var label = (actionLabel || '使用').trim(); + if (label.indexOf('预览') >= 0 || label.indexOf('导出') >= 0) return '预览、打印与导出成品'; + if (label.indexOf('保存') >= 0 || label.indexOf('另存') >= 0) return '保存或另存到云端'; + if (label.indexOf('下载') >= 0) return '下载设计文件'; + return label; + } + + function shellHtml(actionLabel) { + var feat = GATE_FEATURES.map(function (t) { + return '
  • ' + esc(t) + '
  • '; + }).join(''); + return '
    ' + + '
    ' + + '订阅功能' + + '

    开通订阅,解锁完整能力

    ' + + '

    免费版可使用设计工具;订阅后可' + esc(actionCopy(actionLabel)) + '。

    ' + + '
      ' + feat + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    订阅方案

    ' + + '

    选择订阅周期,功能相同,随时续订

    ' + + '
    ' + + '

    正在加载订阅方案…

    ' + + '
    ' + + '
    ' + + '' + + '' + + '
    '; + } + + function findPlan(id) { + var pid = Number(id); + for (var i = 0; i < _gate.plans.length; i++) { + if (_gate.plans[i].id === pid) return _gate.plans[i]; + } + return null; + } + + function planPriceDisplay(plan) { + if (!plan) return ''; + return plan.price_display || (plan.price_cents / 100).toFixed(2); + } + + function renderPlanPicker(modal) { + var picker = modal.querySelector('[data-role="plan-picker"]'); + if (!picker || !plansUi) return; + if (!_gate.plans.length) { + picker.innerHTML = '

    暂无可用订阅方案

    '; + return; + } + picker.innerHTML = plansUi.plansGridHtml(_gate.plans, { + allPlans: _gate.plans, + selectedId: _gate.selectedId, + mode: 'gate', + }); + } + + function updatePayButton(modal) { + var btn = modal.querySelector('[data-action="go-pay"]'); + var plan = findPlan(_gate.selectedId); + if (!btn) return; + if (!plan) { + btn.disabled = true; + btn.textContent = '立即支付'; + return; + } + btn.disabled = false; + btn.textContent = '立即支付 ¥' + planPriceDisplay(plan); + } + + function selectPlan(modal, planId) { + var plan = findPlan(planId); + if (!plan) return; + _gate.selectedId = plan.id; + renderPlanPicker(modal); + updatePayButton(modal); + } + + function closeGate(fireStay) { + var idx = _gate.index; + var payIdx = _gate.payIndex; + var opts = _gate.gateOpts; + _gate = emptyGate(); + if (typeof layer !== 'undefined') { + if (idx != null) layer.close(idx); + if (payIdx != null) layer.close(payIdx); + } + if (fireStay && opts && typeof opts.onStay === 'function') opts.onStay(); + } + + function onPaySuccess() { + var gateIdx = _gate.index; + var opts = _gate.gateOpts; + _gate = emptyGate(); + if (typeof layer !== 'undefined') { + if (gateIdx != null) layer.close(gateIdx); + } + toast('恭喜您,订阅已成功生效!', 'success'); + if (typeof window.soonLoadMembership === 'function') { + window.soonLoadMembership(true).then(function () { + if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity(); + }); + } + if (opts && typeof opts.onSuccess === 'function') opts.onSuccess(); + } + + function openPayStep(modal) { + var plan = findPlan(_gate.selectedId); + if (!plan || !pay || !pay.openPayModal) { + toast('请先选择订阅方案', 'warn'); + return; + } + if (_gate.payIndex != null) { + try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ } + _gate.payIndex = null; + } + var priceDisplay = planPriceDisplay(plan); + _gate.payIndex = pay.openPayModal({ + planId: plan.id, + planName: plan.name, + priceDisplay: priceDisplay, + shadeClose: true, + onPaid: onPaySuccess, + onClose: function () { + _gate.payIndex = null; + updatePayButton(modal); + }, + }); + } + + function renderPlansError(modal, message) { + var picker = modal.querySelector('[data-role="plan-picker"]'); + if (!picker) return; + picker.innerHTML = '

    ' + esc(message) + + '

    '; + updatePayButton(modal); + } + + function loadPlans(modal) { + var picker = modal.querySelector('[data-role="plan-picker"]'); + if (!picker) return; + if (!pay || !plansUi) { + renderPlansError(modal, '订阅模块未加载,请刷新页面'); + return; + } + picker.innerHTML = '

    正在加载订阅方案…

    '; + updatePayButton(modal); + pay.apiGet('/plans').then(function (ps) { + if (!ps.ok) { + renderPlansError(modal, ps.message || '方案加载失败'); + return; + } + var paid = ((ps.data && ps.data.items) || []).filter(function (p) { + return p.code !== 'free' && p.price_cents > 0; + }); + if (!paid.length) { + renderPlansError(modal, '暂无可用订阅方案'); + return; + } + _gate.plans = paid; + var def = paid[0]; + for (var i = 0; i < paid.length; i++) { + if (paid[i].is_recommended) { def = paid[i]; break; } + } + selectPlan(modal, def.id); + }).catch(function () { + renderPlansError(modal, '网络错误,请稍后重试'); + }); + } + + function bindModal(modal) { + modal.addEventListener('click', function (e) { + var stay = e.target.closest('[data-action="stay"]'); + if (stay) { + e.preventDefault(); + closeGate(true); + return; + } + var retry = e.target.closest('[data-action="retry-plans"]'); + if (retry) { + e.preventDefault(); + loadPlans(modal); + return; + } + var goPay = e.target.closest('[data-action="go-pay"]'); + if (goPay) { + e.preventDefault(); + if (!goPay.disabled) openPayStep(modal); + return; + } + var pick = e.target.closest('[data-action="pick-plan"]'); + if (pick) { + e.preventDefault(); + var id = Number(pick.dataset.id); + if (id) selectPlan(modal, id); + } + }); + loadPlans(modal); + } + + function openGate(opts) { + opts = opts || {}; + if (_gate.index != null) { + try { layer.close(_gate.index); } catch (e) { /* ignore */ } + } + if (_gate.payIndex != null) { + try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ } + } + _gate = emptyGate(); + _gate.gateOpts = opts; + + var width = Math.min(1000, window.innerWidth - 24); + layer.open({ + type: 1, + skin: 'soon-layer', + title: false, + closeBtn: 1, + shadeClose: true, + area: [width + 'px', 'auto'], + content: shellHtml(opts.action), + success: function (layero, index) { + var layerEl = layero && layero[0] ? layero[0] : layero; + if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe'); + var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null; + if (content) content.style.padding = '0'; + var modal = layerEl.querySelector('.soon-subscribe-modal'); + if (!modal) return; + _gate.index = index; + _gate.gateOpts = opts; + bindModal(modal); + }, + end: function () { + var payIdx = _gate.payIndex; + var closedOpts = _gate.gateOpts; + if (payIdx != null) { + try { layer.close(payIdx); } catch (e) { /* ignore */ } + } + _gate = emptyGate(); + if (closedOpts && closedOpts.onClose) closedOpts.onClose(); + }, + }); + } + + function soonShowSubscribeGate(opts) { + if (!pay || !plansUi) { + toast('订阅功能暂不可用,请刷新页面后重试', 'warn'); + return false; + } + ensureLayer(function () { openGate(opts || {}); }); + return false; + } + + window.soonShowSubscribeGate = soonShowSubscribeGate; + + if (!window._soonSubscribeFocusBound) { + window._soonSubscribeFocusBound = true; + window.addEventListener('focus', function () { + if (typeof window.soonLoadMembership !== 'function') return; + window.soonLoadMembership(true).then(function () { + if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity(); + }); + }); + } +})(); diff --git a/frontend-web/js/common/portal-auth.js b/frontend-web/js/common/portal-auth.js new file mode 100644 index 0000000..130a30d --- /dev/null +++ b/frontend-web/js/common/portal-auth.js @@ -0,0 +1,81 @@ +(function () { + 'use strict'; + + function showAuthedNav() { + var guest = document.getElementById('auth_guest'); + var account = document.getElementById('auth_account'); + var session = document.getElementById('auth_session'); + if (guest) guest.style.display = 'none'; + if (account) account.style.display = 'inline-flex'; + if (session) session.style.display = 'inline-flex'; + } + + function clearSession() { + if (typeof window.soonClearAuthSession === 'function') { + window.soonClearAuthSession(); + } else { + localStorage.removeItem('soon_access'); + localStorage.removeItem('soon_refresh'); + } + } + + function initTopbar(options) { + options = options || {}; + var tok = localStorage.getItem('soon_access') || ''; + var login = document.getElementById('auth_login'); + var reg = document.getElementById('auth_register'); + var member = document.getElementById('auth_member'); + var admin = document.getElementById('auth_admin'); + var logout = document.getElementById('auth_logout'); + var avatar = document.getElementById('auth_avatar'); + + if (!tok) return; + + showAuthedNav(); + if (login) login.style.display = 'none'; + if (reg) reg.style.display = 'none'; + if (member) member.style.display = 'inline-flex'; + if (logout) logout.style.display = 'inline-flex'; + + var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || ''; + if (!base) return; + + var meFetch = typeof window.soonAuthedFetch === 'function' + ? window.soonAuthedFetch(base + '/auth/me', { headers: { Authorization: 'Bearer ' + tok } }) + : fetch(base + '/auth/me', { headers: { Authorization: 'Bearer ' + tok } }); + meFetch + .then(function (r) { return r.json(); }) + .then(function (j) { + if (!j.ok || !j.data) { + clearSession(); + location.reload(); + return; + } + if (j.data.role === 'admin' && admin) admin.style.display = 'inline-flex'; + if (avatar && j.data.email) { + avatar.style.display = 'inline-flex'; + avatar.textContent = j.data.email.charAt(0).toUpperCase(); + avatar.title = j.data.email; + } + if (typeof window.soonLoadMembership === 'function') { + window.soonLoadMembership().then(function () { + if (typeof window.soonRefreshPortalIdentity === 'function') { + window.soonRefreshPortalIdentity(); + } + }); + } + }) + .catch(function () { /* keep authed shell */ }); + + if (logout) { + logout.addEventListener('click', function (e) { + e.preventDefault(); + clearSession(); + if (options.logoutReload !== false) location.reload(); + else location.href = 'index.web.html'; + }); + } + } + + window.soonPortalAuth = { init: initTopbar }; +})(); diff --git a/frontend-web/js/common/portal-topbar.js b/frontend-web/js/common/portal-topbar.js new file mode 100644 index 0000000..a1be4d0 --- /dev/null +++ b/frontend-web/js/common/portal-topbar.js @@ -0,0 +1,106 @@ +(function () { + 'use strict'; + + var STANDARD_LINKS = [ + { id: 'home', href: 'index.web.html', label: '设计首页', show: 'always' }, + { id: 'member', href: 'member.web.html', label: '订阅', show: 'authed', elId: 'auth_member' }, + { id: 'admin', href: 'admin/index.html', label: '管理', show: 'admin', elId: 'auth_admin' }, + ]; + + function esc(s) { + if (s == null) return ''; + return String(s).replace(/&/g, '&').replace(/' + + '
    登录' + + '注册' + + '
    ' + + '' + + ''; + } + + function renderLinks(links, active) { + return links.map(function (lnk) { + if (lnk.id === 'member' || lnk.id === 'admin') return ''; + var cls = 'soon-portal-topbar__link' + (active === lnk.id ? ' is-active' : ''); + return '' + esc(lnk.label) + ''; + }).join(''); + } + + function toolsBlock(lang, linksHtml) { + return '
    ' + + lang + + (linksHtml ? '' : '') + + '
    '; + } + + /** + * @param {object} opts + * @param {string} [opts.variant] standard | auth | design + * @param {string} [opts.active] home | member + * @param {string} [opts.extraClass] + */ + function html(opts) { + opts = opts || {}; + var base = opts.assetBase || '../'; + var variant = opts.variant || 'standard'; + var extraCls = opts.extraClass ? ' ' + opts.extraClass : ''; + + if (variant === 'auth') { + return '
    ' + + '' + + '
    ' + + toolsBlock('', '返回首页') + + '
    '; + } + + if (variant === 'design') { + return '
    ' + + '' + + '
    ' + + toolsBlock('', '返回首页') + + authBlock() + '
    '; + } + + var active = opts.active || ''; + var showLang = opts.showLanguage !== false; + var links = opts.links || STANDARD_LINKS; + var lang = showLang + ? '' + : ''; + + return '
    ' + + '' + + '
    ' + + toolsBlock(lang, renderLinks(links, active)) + + authBlock() + '
    '; + } + + function mount(selector, opts) { + var el = typeof selector === 'string' ? document.querySelector(selector) : selector; + if (!el) return; + el.innerHTML = html(opts); + if (opts && opts.initLang !== false) { + var sel = document.getElementById('language_select'); + if (sel) { + sel.value = localStorage.getItem('lang') || 'zh'; + sel.onchange = function () { localStorage.setItem('lang', sel.value); }; + } + } + } + + window.SoonPortalTopbar = { html: html, mount: mount }; +})(); diff --git a/frontend-web/js/common/soon-toast.js b/frontend-web/js/common/soon-toast.js new file mode 100644 index 0000000..73c8887 --- /dev/null +++ b/frontend-web/js/common/soon-toast.js @@ -0,0 +1,20 @@ +(function () { + 'use strict'; + + function soonToast(message, type) { + if (typeof layer === 'undefined' || !layer.msg) return; + var skin = 'soon-toast'; + if (type === 'success') skin += ' soon-toast--success'; + else if (type === 'error') skin += ' soon-toast--error'; + else if (type === 'warn') skin += ' soon-toast--warn'; + layer.msg(message, { + time: 2400, + shade: 0, + offset: '72px', + icon: -1, + skin: skin + }); + } + + window.soonToast = soonToast; +})(); diff --git a/frontend-web/js/common/utils.js b/frontend-web/js/common/utils.js new file mode 100644 index 0000000..c8f6d38 --- /dev/null +++ b/frontend-web/js/common/utils.js @@ -0,0 +1,212 @@ +// 公共工具函数模块 + +/** + * DES加密函数 + */ +function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + var APIFMS; + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + } catch (err) { + console.log('des 加密 -------------------------'); + console.log(err); + } + return APIFMS; +} + +/** + * 获取URL参数 + */ +function GetFile() { + const params = new URLSearchParams(window.location.search); + return params; +} + +/** + * 日期格式化 + */ +function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); +} + +/** + * 获取对象的绝对坐标 + */ +function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")]; +} + +/** + * 计算两点之间的距离 + */ +function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; +} + +/** + * 计算角度(相对于画布中心) + */ +function getDeg(pointer, canvas) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90; + return angleDeg; +} + +/** + * 检查字段名是否重复 + */ +function checkName(name, objs1, objs2) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; +} + +/** + * 返回不重复的字段名 + */ +function resName(pre_name, objs1, objs2) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num, objs1, objs2));//重复了就再加1 + return pre_name + num; +} + +/** + * 获取对象在画布中的索引 + */ +function getIndex(target, canvas, _canvas = null) { + let temp_objs; + if (_canvas == null) { + temp_objs = canvas.getObjects(); + } else { + temp_objs = _canvas.getObjects(); + } + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; +} + +/** + * 获取对象类型 + */ +function getType(target, canvas, objs) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } +} + +/** + * 获取坐标的最小X值 + */ +function getCoordsMinX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x; + } + } + return x; +} + +/** + * 获取坐标的最大X值 + */ +function getCoordsMaxX(acoords) { + let x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x; + } + } + return x; +} + +/** + * 获取坐标的最小Y值 + */ +function getCoordsMinY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y; + } + } + return y; +} + +/** + * 获取坐标的最大Y值 + */ +function getCoordsMaxY(acoords) { + let y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y; + } + } + return y; +} + +module.exports = { + desEncrypt, + GetFile, + getDate, + getAbsoluteXY, + getDisdance, + getDeg, + checkName, + resName, + getIndex, + getType, + getCoordsMinX, + getCoordsMaxX, + getCoordsMinY, + getCoordsMaxY +}; + diff --git a/frontend-web/js/design1-back.js b/frontend-web/js/design1-back.js new file mode 100644 index 0000000..c89a050 --- /dev/null +++ b/frontend-web/js/design1-back.js @@ -0,0 +1,5781 @@ + + function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") { + var APIFMS; + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key); + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + } catch (err) { + console.log('des 加密 -------------------------'); + console.log(err); + } + return APIFMS; + } + (function (fabric) { + fabric.CurvedText = fabric.util.createClass(fabric.Object, { + type: 'curved-text', + diameter: 0, + kerning: 0, + text: '', + flipped: false, + fill: '#000', + fontFamily: 'Times New Roman', + fontSize: 24, // in px + fontWeight: 'normal', + fontStyle: '', // "normal", "italic" or "oblique". + cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), + strokeStyle: null, + strokeWidth: 0, + initialize: function (text, options) { + options || (options = {}); + if (typeof text === 'object' && text) { + for (let key in text) { + this[key] = text[key] + } + } else { + this.text = text; + } + this.callSuper('initialize', options); + this.set('lockUniScaling', true); + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + }, + _getFontDeclaration: function () { + return [ + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + cropCanvas: function (canvas) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + pix = { x: [], y: [] }, n, + imageData = ctx.getImageData(0, 0, w, h), + fn = function (a, b) { return a - b }; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + if (imageData.data[((y * w + x) * 4) + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // 如果没有找到任何非透明像素,直接返回,不进行裁剪 + if (pix.x.length === 0 || pix.y.length === 0) { + return; + } + pix.x.sort(fn); + pix.y.sort(fn); + n = pix.x.length - 1; + w = pix.x[n] - pix.x[0]; + h = pix.y[n] - pix.y[0]; + // 确保宽度和高度是有效的正数 + if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { + return; + } + var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); + canvas.width = w; + canvas.height = h; + ctx.putImageData(cut, 0, 0); + }, + getCircularText: function () { + var text = this.text, + diameter = this.diameter, + flipped = this.flipped, + kerning = this.kerning, + fill = this.fill, + inwardFacing = true, + startAngle = 0, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'), + cw, // character-width + x, // iterator + clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise + if (flipped) { + // startAngle = 180; + inwardFacing = false; + } + startAngle *= Math.PI / 180; // convert to radians + var d = document.createElement('div'); + d.style.fontFamily = this.fontFamily; + d.style.fontSize = this.fontSize + 'px'; + d.style.fontWeight = this.fontWeight; + d.style.fontStyle = this.fontStyle; + d.textContent = text; + document.body.appendChild(d); + var textHeight = d.offsetHeight; + document.body.removeChild(d); + canvas.width = canvas.height = diameter; + ctx.font = this._getFontDeclaration(); + if (inwardFacing) { text = text.split('').reverse().join('') }; + ctx.translate(diameter / 2, diameter / 2); // Move to center + startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward + ctx.textBaseline = 'middle'; // Ensure we draw in exact center + ctx.textAlign = 'center'; // Ensure we draw in exact center + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; + startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; + } + ctx.rotate(startAngle); + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; // half letter + ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); + if (this.strokeStyle && this.strokeWidth) { + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.miterLimit = 2; + ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + } + ctx.fillStyle = fill; + ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter + } + return canvas; + }, + _set: function (key, value) { + switch (key) { + case 'scaleX': + this.fontSize *= value; + this.diameter *= value; + this.width *= value; + this.scaleX = 1; + if (this.width < 1) { this.width = 1; } + break; + case 'scaleY': + this.height *= value; + this.scaleY = 1; + if (this.height < 1) { this.height = 1; } + break; + default: + this.callSuper('_set', key, value); + break; + } + }, + _render: function (ctx) { + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); + this.setCoords(); + }, + toObject: function (propertiesToInclude) { + return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); + } + }); + fabric.CurvedText.fromObject = function (object, callback, forceAsync) { + return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); + }; + })(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); + + const remote = require('@electron/remote') + var path = require('path'); + // 使用用户数据目录,避免写入需要管理员权限 + const exePath = remote.app.getPath('userData') + const { ipcRenderer } = require('electron') + const { dialog } = require('@electron/remote') + var jrQrcode = require('jr-qrcode'); + var JsBarcode = require('jsbarcode'); + ipcRenderer.send('get-sys-fonts'); + ipcRenderer.send('get-scale-rate'); + const { clipboard } = require('electron') + const dpi = 600 + // // 内部剪贴板 + // let clipboardData = { + // objects: null, + // offset: 10 // 粘贴位置偏移量 + // }; + + layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { + let myDate = new Date(); + let s_lan = ""; + ipcRenderer.send('get-sys-language'); + /* if(myDate.getFullYear()>2021 && myDate.getMonth() >= 1 && myDate.getDate() > 15){ + + }*/ + function GetFile() { + const params = new URLSearchParams(window.location.search); + return params + } + function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)); + }) + + $("[language='t']").each(function (i) { + $(this).attr("title", $(this).attr(lan)); + }) + } + function language_str(str) { + let t = { + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" }, + "save": { zh: "保存", ozh: "保存", en: "Save" }, + "noSave": { zh: "不保存", ozh: "不保存", en: "No saving" }, + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + "saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" }, + "saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" }, + "display": { zh: "预览", ozh: "預覽", en: "Display" }, + "output": { zh: "导出", ozh: "導出", en: "Export" }, + "bg": { zh: "背景", ozh: "背景", en: "Background" }, + "selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" }, + "selectPic": { zh: "请选择文件", ozh: "請選擇文件", en: "Please select a file" }, + "img": { zh: "圖片", ozh: "圖片", en: "Image" }, + "simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" }, + "text": { zh: "文本", ozh: "文本", en: "Text" }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + "stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" }, + "rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" }, + "circ": { zh: "圓形", ozh: "圓形", en: "Circle" }, + "line": { zh: "直線", ozh: "直線", en: "Line" }, + "qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" }, + "barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" }, + "counter": { zh: "計數器", ozh: "计数器", en: "Counter" }, + "about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" }, + "vers": { zh: "版本", ozh: "版本", en: "Version" }, + "comf": { zh: "确认", ozh: "確認", en: "Select" }, + "copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + } + return t[str][s_lan]; + } + + var $ = layui.$; + var layer = layui.layer + , slider = layui.slider, colorpicker = layui.colorpicker; + let addState = 0;//0是不添加 + let background_image, background_image1, background_image2; + let bg_version = 1;//front_bg1.png front_bg2.png + var zoom = 1; + $(function () { + // 定义CurvedText类 + + + $("#object_attribute").css("height", "470px"); + $("#obj_list").css("height", document.body.clientHeight - 520); + $(".obj_list").css("height", $("#obj_list").height() - 32); + + $(".main").show(); + $(".canvas_box").css("height", document.body.clientHeight - 100); + zoom = $(".canvas_bg_img").width() / 2787 + $("#canvas1").attr("width", $(".canvas_box").width() / zoom); + $("#canvas1").attr("height", $(".canvas_box").height() / zoom); + $("#canvas2").attr("width", $(".canvas_box").width() / zoom); + $("#canvas2").attr("height", $(".canvas_box").height() / zoom); + var openAs = { + _name: "", + set name(val) { + if (val == "") { + $("title").html('Soon Design'); + } else { + $("title").html('Soon Design - ' + val); + } + this._name = val; + }, + get name() { + return this._name + }, + };//被打开的文件名 + let objs1 = []; + let objs2 = []; + let step1 = { val: 0 }, step2 = { val: 0 }; + let step = step1; + let objs = objs1; + let recordJson1 = [], recordJson2 = [], recordJson = recordJson1, recordObjs1 = [], recordObjs2 = [], recordObjs = recordObjs1; + let pre_objs1 = [], pre_objs2 = [], next_objs1 = [], next_objs2 = [], pre_objs = pre_objs1, next_objs = next_objs1; + let pre_json1 = [], pre_json2 = [], next_json1 = [], next_json2 = [], pre_json = pre_json1, next_json = next_json1; + var canvas1 = new fabric.Canvas('canvas1'); + var canvas2 = new fabric.Canvas('canvas2'); + fabric.Object.prototype.objectCaching = false; + var ctx1 = canvas1.getSelectionContext(); + var ctx2 = canvas2.getSelectionContext(); + let is_bgi_add = false; + canvas1.zoomToPoint(new fabric.Point(0, 0), 1); + canvas2.zoomToPoint(new fabric.Point(0, 0), 1); + + // 初始化标尺 + initRulers(); + + $("#source_front").width(zoom * $("#source_front").width()); + $("#source_back").width(zoom * $("#source_back").width()); + $(".canvas-container:eq(0)").css('transform', 'scale(' + zoom + ')') + $(".canvas-container:eq(1)").css('transform', 'scale(' + zoom + ')') + $(".canvas-container:eq(0)").css('width', 0) + + $(".canvas-container:eq(1)").hide(); + var canvas = canvas1; + fabric.Object.prototype.set({ + borderColor: "#9013FE", + cornerColor: "#9013FE", //激活状态角落图标的填充颜色 + cornerStrokeColor: "#9013FE", //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 6, + cornerSize: 24, + cornerStyle: "circle", //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0, + }); + canvas1.preserveObjectStacking = true; + canvas2.preserveObjectStacking = true; + + + var front_list = []; + ipcRenderer.on('font-list', (event, data) => { + const newFontList = []; + data.map(item => { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')); + } else { + newFontList.push(item); + } + }) + front_list = newFontList; + for (let item of front_list) { + $("#text_font_family").append(""); + $("#circle_text_font_family").append(""); + $("#out_text_font_family").append(""); + $("#barcode_font_family").append(""); + $("#count_font_family").append(""); + } + }); + ipcRenderer.on('sys-lan', (event, data) => { + let lang = localStorage.getItem("lang"); + if (lang) { + s_lan = lang; + langua_ge(lang); + $("#language_select").val(lang); + return; + } + switch (data) { + case 'zh-CN': + s_lan = "zh"; + langua_ge("zh"); + $("#language_select").val("zh"); + localStorage.setItem("lang", "zh"); + break; + case 'zh': + s_lan = "zh"; + langua_ge("zh"); + $("#language_select").val("zh"); + localStorage.setItem("lang", "zh"); + break; + case 'zh-TW': + s_lan = "ozh"; + langua_ge("ozh"); + $("#language_select").val("ozh"); + localStorage.setItem("lang", "ozh"); + break; + case 'en': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-AU': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-CA': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-GB': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-NZ': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-US': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + case 'en-ZA': + s_lan = "en"; + langua_ge("en"); + $("#language_select").val("en"); + localStorage.setItem("lang", "en"); + break; + } + }); + ipcRenderer.on('close', (event, message) => { + layer.confirm('' + language_str("whetherSave") + '', {//是否保存当前文件? + btn: [language_str("save"), language_str("noSave"), language_str("cancel")] //['保存', '不保存', '取消'] + , btn3: function (index, layero) { + + } + }, function (index, layero) { + + output(() => ipcRenderer.send('run-close')); + return; + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + if (openAs.name != "") { + //打开的文件 + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + + ipcRenderer.send('run-close'); + return; + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { ipcRenderer.send('run-close'); return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//保存成功至 + saveHistory(); + ipcRenderer.send('run-close'); + }).catch(err => { + console.log() + }) + layer.close(index); + }, function (index) { + ipcRenderer.send('run-close'); + }); + + }) + addBackground();//添加背景 白色卡片 + + initAligningGuidelines(canvas1); + initAligningGuidelines(canvas2); + //canvas.setZoom(0.5) + //recordObjs1.push(JSON.stringify(objs1)); + //recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //recordObjs2.push(JSON.stringify(objs2)); + //recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + canvas.renderAll();//重新渲染画布 + var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + console.log(val, zoomSlider) + canvas.zoomToPoint( + new fabric.Point(0, 0), + val / 100 + ); + // zoom = val / 100; + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } + + }); + window.rotate = function (id) { + let angle = $(`#${id}`).attr("angle") - 0; + angle += 90; + $(`#${id}`).css("transform", `rotate(${angle}deg)`); + $(`#${id}`).attr("angle", angle); + } + $('#display').on('click', function () { + // 类型改变 + fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + + fabric.Image.fromURL('./public/images/back_bg_1.png', function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + fabric.Image.fromURL('./public/images/op_1.png', function (i3) { + i2.left = background_image.left; + i2.top = background_image.top; + let img3 = i3; + display_func(img1, img2, img3); + }) + }) + }) + }); + // 导出文件 + async function saveImageAsPNG(buffer) { + let fs = require('fs'); + // 弹出保存对话框 + const { canceled, filePath } = await dialog.showSaveDialog({ + title: '保存图片', + defaultPath: path.join(exePath, 'output.png'), // 默认路径 + filters: [{ name: 'PNG Images', extensions: ['png'] }] // 文件过滤器 + }); + + if (canceled) { + console.log('用户取消了保存操作'); + return; + } + + // 将Buffer写入PNG文件到用户选择的路径 + fs.writeFile(filePath, buffer, (err) => { + if (err) { + console.error('写入文件失败:', err); + } else { + layer.msg(language_str("saveSucc") + filePath);//'保存成功至' + console.log('PNG文件已保存至:', filePath); + } + }); + } + function display_func(img1, img2, img3) { + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = []; + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + let list1 = [..._objs1] + for (let item of list1) { + let idx = getIndex(item, canvas1); + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = []; + left = 0; + top = 0; + black_top = 0; + black_left = 0; + all_top = 0; + all_left = 0; + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//黑色字或计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && (objs2[idx - 1].type == 10)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let fs = require('fs'); + const Buffer = require('buffer').Buffer; + const buffer = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + const printPath = path.join(exePath, 'print.png'); + fs.writeFileSync(printPath, buffer); + + layer.open({ + type: 1, + area: [w, h], + title: language_str("display"), //不显示标题栏"预览" + shadeClose: true, //点击遮罩关闭 + content: ` +
    +
    +
    + +
    + +
    +
    +
    +
    `, + btn: [language_str("output"), "打印"],//'导出' + btn1: function (index, layero) { + saveImageAsPNG(buffer) + + printJS({ printable: printPath, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } + }); + } + function output(callback = null, _save = save) { + // 类型改变 + fabric.Image.fromURL('./public/images/op_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + fabric.Image.fromURL('./public/images/op_1.png', function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = [];//黑色图层 + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + item.get("fill") == "#000000" + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + item.get("fill") == "#000000" + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = [];//黑色图层 + left = 0; + top = 0; + black_left = 0;//黑色左裁剪 + black_top = 0;//黑色顶裁剪 + all_left = 0;//黑色左裁剪 + all_top = 0;//黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + // let white_url2 = canvas2.getObjects()[0].toDataURL({ + // height: 2787, + // width: 2787, + // }); + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; + if (objs1.length != 0 & objs2.length == 0) { + op.flag = 2; + } else if (objs1.length == 0 & objs2.length != 0) { + op.flag = 3; + } + op.frontColorPic = desEncrypt(url1); + op.backColorPic = ''; + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = ''; + op.backBlackPic = ''; + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + if (idx == 0) continue;//background-image + let type = objs1[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 1, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + + } else if (type == 11) { + + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 3, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "Exceed": objs1[idx - 1].exceed, + "AutoWrap": objs1[idx - 1].autoWrap, + "FixSize": objs1[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs1[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 4, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].color, + "DefaultText": objs1[idx - 1].val, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 5, + "name": objs1[idx - 1].name, + "FontSize": parseInt(objs1[idx - 1].fontSize), + "DefaultText": objs1[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontFamily": objs1[idx - 1].font, + "FontColor": objs1[idx - 1].color, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 6, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": objs1[idx - 1].start, + "add": parseInt(objs1[idx - 1].add), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (idx == 0) continue;//background-image + let type = objs2[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 1, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + + } else if (type == 11) { + + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 3, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontSize": parseInt(item.get("fontSize")), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontColor": item.get("fill"), + "Exceed": objs2[idx - 1].exceed, + "AutoWrap": objs2[idx - 1].autoWrap, + "FixSize": objs2[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs2[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 4, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].color, + "DefaultText": objs2[idx - 1].val, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 5, + "name": objs2[idx - 1].name, + "DefaultText": objs2[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontSize": parseInt(objs2[idx - 1].fontSize), + "FontFamily": objs2[idx - 1].font, + "FontColor": objs2[idx - 1].color, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 6, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": parseInt(objs2[idx - 1].start), + "add": parseInt(objs2[idx - 1].add), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + _save(op, callback); + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + } + $('#output').on('click', function () { + output(null, saveAs); + return; + // 类型改变 + fabric.Image.fromURL('./public/images/op_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + fabric.Image.fromURL('./public/images/op_1.png', function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = [];//黑色图层 + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = [];//黑色图层 + left = 0; + top = 0; + black_left = 0;//黑色左裁剪 + black_top = 0;//黑色顶裁剪 + all_left = 0;//黑色左裁剪 + all_top = 0;//黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + // let white_url2 = canvas2.getObjects()[0].toDataURL({ + // height: 2787, + // width: 2787, + // }); + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; + if (objs1.length != 0 & objs2.length == 0) { + op.flag = 2; + } else if (objs1.length == 0 & objs2.length != 0) { + op.flag = 3; + } + op.frontColorPic = desEncrypt(url1); + op.backColorPic = ''; + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = ''; + op.backBlackPic = ''; + for (let item of _objs1) { + let idx = getIndex(item, canvas1); + if (idx == 0) continue;//background-image + let type = objs1[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 1, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 3, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "Exceed": objs1[idx - 1].exceed, + "AutoWrap": objs1[idx - 1].autoWrap, + "FixSize": objs1[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs1[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 4, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].color, + "DefaultText": objs1[idx - 1].val, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 5, + "name": objs1[idx - 1].name, + "FontSize": parseInt(objs1[idx - 1].fontSize), + "DefaultText": objs1[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontFamily": objs1[idx - 1].font, + "FontColor": objs1[idx - 1].color, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 6, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": objs1[idx - 1].start, + "add": parseInt(objs1[idx - 1].add), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2); + if (idx == 0) continue;//background-image + let type = objs2[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 1, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 3, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontSize": parseInt(item.get("fontSize")), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontColor": item.get("fill"), + "Exceed": objs2[idx - 1].exceed, + "AutoWrap": objs2[idx - 1].autoWrap, + "FixSize": objs2[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs2[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 4, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].color, + "DefaultText": objs2[idx - 1].val, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 5, + "name": objs2[idx - 1].name, + "DefaultText": objs2[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontSize": parseInt(objs2[idx - 1].fontSize), + "FontFamily": objs2[idx - 1].font, + "FontColor": objs2[idx - 1].color, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 6, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": parseInt(objs2[idx - 1].start), + "add": parseInt(objs2[idx - 1].add), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'CS File Type', extensions: ['cs'] }, + ] + }).then(result => { + if (result.canceled) return; + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.cs'; + } + let fs = require('fs') + op.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(op)) + }).catch(err => { + console.log() + }) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + }); + var _keyctrl = false, _keyc = false, _textEdit = false, _inputEdit = false; + $("input").focus(function () { + console.log(1) + _inputEdit = true + }) + $("input").blur(function () { + _inputEdit = false + }) + $("textarea").focus(function () { + console.log(1) + _inputEdit = true + }) + $("textarea").blur(function () { + _inputEdit = false + }) + $("body").keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false; + }; + if (event.keyCode == 67) { + _keyc = false; + }; + }) + $("body").keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $("#new").click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $("#open").click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $("#save").click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + output(null, saveAs); + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $("#previous").click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $("#next").click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + pasteFromClipboard(); + return; + } + + // CTRL/CMD + SHIFT + DEL (Windows) 或 CMD + SHIFT + Backspace (Mac) - 清空 + if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#clean").click(); + return; + } + + // CTRL/CMD + DEL (Windows) 或 CMD + Backspace (Mac) - 删除 + if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#delete").click(); + return; + } + + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true; + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 67) { + _keyc = true; + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 37) { + //left + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + let x = canvas.getActiveObject().get("left"); + x += v; + canvas.getActiveObject().set("left", x); + canvas.renderAll(); + recordState(); + updateControls(); + } + if (event.keyCode == 38) { + //up + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + let y = canvas.getActiveObject().get("top"); + y += v; + canvas.getActiveObject().set("top", y); + canvas.renderAll(); + recordState(); + updateControls(); + } + if (event.keyCode == 39) { + //right + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + let x = canvas.getActiveObject().get("left"); + x += v; + canvas.getActiveObject().set("left", x); + canvas.renderAll(); + recordState(); + updateControls(); + } + if (event.keyCode == 40) { + //down + let v = +1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + let y = canvas.getActiveObject().get("top"); + y += v; + canvas.getActiveObject().set("top", y); + canvas.renderAll(); + recordState(); + updateControls(); + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + let actObjs = canvas.getActiveObjects(); + let allObjs = canvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1); + canvas.remove(item); + flag = 1; + + } + } + updateList(); + canvas.discardActiveObject(); + if (flag == 1) { + recordState(); + canvas.renderAll(); + handleObjectSelected() + } + } + }); + let pre_add_image; + $("#addPic").click(function () { + OpenDialog(); + function OpenDialog() { + dialog.showOpenDialog({ + title: language_str("selectPic"),//请选择图片 + buttonLabel: language_str("comf"),//"确认" + filters: [ + { name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }, + ] + }).then(result => { + if (result.canceled) return; + //console.log(result.filePaths) + fabric.Image.fromURL(result.filePaths[0], function (image) { + //image.left = pointer.x; + //image.top = pointer.y; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image; + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 1; + }); + //canvas.add(image); + //objs.push({type: 1}); + //canvas.renderAll(); + //updateList(); + //recordState(); + }); + }).catch(err => { + console.log(err) + }) + } + }); + $("#addOutPic").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 2; + }); + $("#addText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 3; + }); + + $("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; + }); + $("#addOutText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 4; + }); + $("#addRect").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 5; + }); + $("#addCircle").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 6; + }); + $("#addLine").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 7; + }); + $("#addQrCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 8; + }); + $("#addBarCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 9; + }); + $("#addCounter").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + addState = 10; + }); + $("#version_change").click(function () { + if (bg_version == 1) { + bg_version = 2; + } else { + bg_version = 1; + } + // 类型改变 + $("#source_front").attr("src", "./public/images/front_bg" + bg_version + "_1.png"); + canvas.renderAll(); + + }); + $("#front_side").click(function () { + $(this).addClass("ui-button-active"); + $("#back_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").show(); + $(".canvas-container:eq(1)").hide(); + console.log("------"); + console.log(recordObjs1); + console.log("------"); + canvas = canvas1; + objs = objs1; + background_image = background_image1; + next_objs = next_objs1; + pre_objs = pre_objs1; + next_json = next_json1; + pre_json = pre_json1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + updateList(); + + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//背景 + }); + $("#back_side").click(function () { + $(this).addClass("ui-button-active"); + $("#front_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").hide(); + $(".canvas-container:eq(1)").show(); + canvas = canvas2; + objs = objs2; + background_image = background_image2; + next_objs = next_objs2; + pre_objs = pre_objs2; + next_json = next_json2; + pre_json = pre_json2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + updateList(); + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + }); + $("#top_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set("top", background_image.get("top")); + canvas.renderAll(); + return; + } + let top = 0; + for (let item of _objs) { + let t_top = item.get("top"); + if (t_top < top) { + top = t_top; + } + } + groupItems = []; + for (let item of _objs) { + item.set("top", top); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + + }); + $("#bottom_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + _objs[0].set("top", background_image.get("top") + background_image.get("height") - _objs[0].get("height") * _objs[0].get("scaleY") - other); + canvas.renderAll(); + return; + } + let bottom = 0; + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + let t_b = item.get("top") + item.get("height") * item.get("scaleY") + other; + if (t_b > bottom) { + bottom = t_b; + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("top", bottom - item.get("height") * item.get("scaleY") - other); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + + }); + $("#center_y_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + _objs[0].set("top", background_image.get("top") + (background_image.get("height") - (_objs[0].get("height") + other) * _objs[0].get("scaleY")) / 2); + canvas.renderAll(); + return; + } + let top = 0; + for (let item of _objs) { + let t_top = item.get("top"); + if (t_top < top) { + top = t_top; + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("top", top + (item.group.height - (item.get("height") + other) * item.get("scaleY")) / 2); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#left_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set("left", background_image.get("left")); + canvas.renderAll(); + return; + } + let left = 0; + for (let item of _objs) { + let t_left = item.get("left"); + if (t_left < left) { + left = t_left; + } + } + groupItems = [] + for (let item of _objs) { + item.set("left", left); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#right_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + _objs[0].set("left", background_image.get("left") + background_image.get("width") - _objs[0].get("width") * _objs[0].get("scaleX") - other); + canvas.renderAll(); + return; + } + let right = 0; + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + let t_r = item.get("left") + item.get("width") * item.get("scaleX") + other; + if (t_r > right) { + right = t_r; + } + } + let groupItems = [] + + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("left", right - item.get("width") * item.get("scaleX") - other); + groupItems.push(item) + } + //BUG1 + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#center_x_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + _objs[0].set("left", background_image.get("left") + (background_image.get("width") - (_objs[0].get("width") + other) * _objs[0].get("scaleX")) / 2); + canvas.renderAll(); + return; + } + let left = 0; + for (let item of _objs) { + let t_left = item.get("left"); + if (t_left < left) { + left = t_left; + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + item.set("left", left + (item.group.width - (item.get("width") + other) * item.get("scaleX")) / 2); + groupItems.push(item) + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + }); + $("#up_obj").click(function () {//上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + //console.log(objs); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + recordState(); + updateList(); + selectObj(idx); + } + }); + $("#down_obj").click(function () {//下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + recordState(); + updateList(); + selectObj(idx - 2); + } + }); + $("#set_bg").click(function () {//设置为背景 + let obj = canvas.getActiveObject(); + console.log(obj, obj.get("selectable")) + let idx = getIndex(obj); + const scaleX = 2787 / obj.width + const scaleY = 2787 / obj.height + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - 2787) / 2, + top: ($('#canvas1').height() - 2787) / 2, + }); + $('#text_w').val(2787) + $('#text_h').val(2787) + $('#text_x').val(0) + $('#text_y').val(0) + if (obj.get("selectable")) { + obj.set("selectable", false); + $("#lock_img").attr("src", "public/images/lock20.png") + } + canvas.sendToBack(obj); + canvas.bringForward(obj); + canvas.renderAll(); + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + recordState(); + updateList(); + selectObj(0); + }); + + // ========================================== + // 【新增】状态恢复后的回调函数 + // 作用:确保图片加载完毕后,再刷新界面,防止白屏或对象丢失 + // ========================================== + function onStateRestored() { + // 1. 重新获取背景图引用 (因为旧对象被销毁了) + // 注意:canvas 是全局变量,指向当前激活的画板 + if (canvas.getObjects().length > 0) { + background_image = canvas.getObjects()[0]; + background_image.selectable = false; // 确保背景不可选 + background_image.evented = false; + } + + // 2. 渲染画布 + canvas.requestRenderAll(); + + // 3. 取消选中状态 + selectObj(-1); + + // 4. 隐藏所有属性面板 + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + + // 5. 刷新左侧列表 + updateList(); + } + // ========================================== + // 撤销操作 (修复版) + // ========================================== + $("#previous").click(function () { + if (step.val == 0) return; + + step.val--; + + // 判断当前是正面还是背面 + if ($("#front_side").hasClass("ui-button-active")) { + // 1. 恢复业务数据 + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + // 2. 恢复画布 (使用回调函数!) + canvas.loadFromJSON(recordJson1[step.val], function() { + onStateRestored(); // 图片加载完才执行这一步 + }); + } else { + // 背面逻辑 + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + canvas.loadFromJSON(recordJson2[step.val], function() { + onStateRestored(); + }); + } + }); + // ========================================== + // 重做操作 (修复版) + // ========================================== + $("#next").click(function () { + // 获取当前历史记录的最大长度 + let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; + + if (step.val >= maxLen - 1) return; + + step.val++; + + if ($("#front_side").hasClass("ui-button-active")) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + canvas.loadFromJSON(recordJson1[step.val], function() { + onStateRestored(); // 图片加载完才执行这一步 + }); + } else { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + canvas.loadFromJSON(recordJson2[step.val], function() { + onStateRestored(); + }); + } + }); + + $("#clean").click(function () { + objs.splice(0, objs.length); + + let allObjs = canvas.getObjects(); + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item); + } + } + updateList(); + recordState(); + }); + $("#rotate").click(function () { + let obj = canvas.getActiveObject(); + obj.rotate(Math.round((obj.get("angle") + 90) % 360)); + $("#text_r").val(obj.get("angle")); + canvas.renderAll(); + updateControls(); + recordState(); + }); + $('#vertical_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipY", !obj.get("flipY")); + canvas.renderAll(); + updateControls(); + recordState(); + }); + $('#align_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipX", !obj.get("flipX")); + canvas.renderAll(); + updateControls(); + recordState(); + }); + $("#text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + + $("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + + $('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + recordState() + canvas.renderAll() + }) + $('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + recordState() + canvas.renderAll() + }) + $('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + recordState() + canvas.renderAll() + }) + + $("#out_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#out_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#out_text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#out_text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#lock").click(function () { + canvas.getActiveObject().set("selectable", !canvas.getActiveObject().get("selectable")); + if (canvas.getActiveObject().get("selectable")) { + $("#lock_img").attr("src", "public/images/unlock20.png")//已经上锁,需要解锁 + } else { + $("#lock_img").attr("src", "public/images/lock20.png")//没上锁,可以上锁 + } + recordState(); + canvas.renderAll(); + }); + $("#text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#text_back_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#circle_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#circle_text_back_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#out_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#out_text_back_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#rect_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#rect_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#rect_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#rect_stroke").val("#ffffff"); + canvas.renderAll(); + }); + $("#circle_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#circle_color").val("#ffffff"); + canvas.renderAll(); + }); + $("#circle_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#circle_stroke").val("#ffffff"); + canvas.renderAll(); + }); + + function saveAs(op1, callback) { + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + let con_o = Object.assign(o, op1); + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + con_o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + if (callback && typeof callback === 'function') { + callback(); + } + }).catch(err => { + console.log() + }) + } + function save(op1, callback) { + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + let con_o = Object.assign(o, op1); + if (openAs.name != "") { + //打开的文件 + let fs = require('fs') + con_o.soonType = 1 + fs.writeFileSync(openAs.name, JSON.stringify(con_o)) + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + if (callback && typeof callback === 'function') { + callback(); + } + return; + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + con_o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + saveHistory(); + if (callback && typeof callback === 'function') { + callback(); + } + }).catch(err => { + console.log() + }) + } + + // =========================================================== + // 1. 剪贴板数据定义 (修改:改为存储数据 data,而不是对象引用) + // =========================================================== + let clipboardData = { + data: null, // 存储序列化后的 JSON 数据 + 业务属性 + offset: 10 // 粘贴位置偏移量 + }; + + // =========================================================== + // 2. 复制函数:深度拷贝当前选中对象的数据 + // =========================================================== + function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + layer.msg(language_str("selectPic") || "请先选择对象"); + return; + } + + // 【核心修复】 + // 遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据 + clipboardData.data = activeObjects.map(obj => { + let idx = getIndex(obj); // 获取对象在画布中的索引 + + // 获取业务属性 (objs数组中对应的那一项) + // 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1 + let appData = (idx > 0 && objs[idx-1]) ? objs[idx-1] : { type: 0 }; + + return { + // 1. 保存画面样式 (坐标、颜色、字号等) + fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data']), + // 2. 保存业务属性 (深拷贝,防止引用冲突) + appData: JSON.parse(JSON.stringify(appData)) + }; + }); + + clipboardData.offset = 10; // 重置偏移量 + console.log(`已复制 ${activeObjects.length} 个对象`); + layer.msg("已复制"); + } + + // =========================================================== + // 3. 粘贴函数:根据保存的数据重建对象 + // =========================================================== + function pasteFromClipboard() { + // 检查是否有数据 + if (!clipboardData.data || clipboardData.data.length === 0) { + // layer.msg("剪贴板为空"); + return; + } + + // 1. 提取 Fabric JSON 准备恢复画面 + const jsonToEnliven = clipboardData.data.map(item => item.fabricJson); + + // 2. 恢复对象 (异步过程) + fabric.util.enlivenObjects(jsonToEnliven, function(objects) { + canvas.discardActiveObject(); + + objects.forEach((clone, index) => { + // --- A. 处理画面位置 --- + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + + // 如果是组内对象,需要特殊处理坐标 + if (clone.group) { + clone.set({ + left: clone.group.left + clipboardData.offset, + top: clone.group.top + clipboardData.offset + }); + } + + // --- B. 处理业务数据 --- + // 取出复制时保存的 appData + let savedAppData = clipboardData.data[index].appData; + let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝 + + // 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy) + if (newAppData.name) { + newAppData.name = newAppData.name + "_copy"; + } + + // --- C. 添加到画布和数据数组 --- + canvas.add(clone); + objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组 + }); + + // 3. 选中新粘贴出来的对象 + let sel = new fabric.ActiveSelection(objects, { canvas: canvas }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + + // 4. 刷新列表和历史记录 + updateList(); + recordState(); + + // 5. 高亮左侧列表最后一项 (即新粘贴的项) + if(objs.length > 0) selectObj(objs.length - 1); + + // 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西) + clipboardData.offset += 10; + }); + } + + // =========================================================== + // 4. 按钮事件绑定 (保持原样) + // =========================================================== + // 复制 + $("#copy").click(function () { + copySelection(); + }) + // 粘贴 + $("#paste").click(function () { + pasteFromClipboard(); + }) + + // 保存 (保持你原有的保存逻辑不变) + $("#save").click(function () { + output(); // 这里调用你的 output 函数 + return; + // ... (下面是你原有的被 return 截断的代码,建议以后清理掉,但这里我按要求保留) + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + // ... (省略你原来那段很长的保存代码) ... + }); + + + function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); + } + function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")] + } + let fullPath = path.join(exePath, 'data.json') + // ========================================== + // 历史记录保存 (修复结构Bug + 自动置顶) + // ========================================== + function saveHistory() { + let fs = require('fs'); + let j = { history: [] }; // 默认结构必须是数组 + + try { + // 1. 读取并解析现有文件 + if (fs.existsSync(fullPath)) { + let content = fs.readFileSync(fullPath).toString(); + // 防止空文件报错 + if (content.trim()) { + j = JSON.parse(content); + } + } + } catch (e) { + console.error("读取历史记录失败,将重置:", e); + // 出错时使用默认空数组,不中断流程 + j = { history: [] }; + } + + // 确保 j.history 是数组 (防御性编程) + if (!Array.isArray(j.history)) { + j.history = []; + } + + // 2. 【核心优化】先过滤掉当前文件(如果已存在),然后加到最前面 + // 这样可以实现“最近使用的文件排在第一位”的效果 + let currentPath = openAs.name; + let newList = j.history.filter(item => item.path !== currentPath); + + // 3. 插入到头部 (Unshift) + newList.unshift({ + time: getDate(), + path: currentPath, + // type: 1 // 如果需要记录文件类型也可以加在这里 + }); + + // 4. (可选) 限制历史记录数量,比如只保留最近20条,防止文件无限膨胀 + if (newList.length > 20) { + newList = newList.slice(0, 20); + } + + j.history = newList; + + // 5. 统一写入 + try { + fs.writeFileSync(fullPath, JSON.stringify(j)); + } catch (e) { + console.error("写入历史记录失败:", e); + } + } + + function open(file) { + console.log(file) + let fs = require('fs') + var fsData = fs.readFileSync(file) + openAs.name = file; + let j = JSON.parse(fsData.toString()); + let left = background_image.left; + let top = background_image.top; + let new_left = j.f.objects[0].left; + let new_top = j.f.objects[0].top; + for (let o of j.f.objects) { + o.left = o.left - new_left + left; + o.top = o.top - new_top + top; + } + // for (let o of j.b.objects) { + // o.left = o.left - new_left + left; + // o.top = o.top - new_top + top; + // } + console.log(j.f) + canvas1.loadFromJSON(j.f); + canvas2.loadFromJSON(j.b); + background_image1 = canvas1.getObjects()[0]; + background_image2 = canvas2.getObjects()[0]; + + objs2 = j.bo; + objs1 = j.fo; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + background_image = background_image1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + background_image = background_image2; + step = step2; + } + canvas1.renderAll(); + canvas2.renderAll(); + updateList(); //修复打开新文件后无法撤销的bug + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j1.objects[0].hoverCursor = "default"; + recordJson1.push(j1); + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + // j2.objects[0].hoverCursor = "default"; + recordJson2.push(j2); + }, 0); + } + $("#open").click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件? + output(OpenDialog); + layer.close(index); + + }, function (index) { + OpenDialog(); + }); + function OpenDialog() { + dialog.showOpenDialog({ + title: "请选择文件",//confirm + buttonLabel: language_str("comf"),//"确认" + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.canceled) return; + console.log(result.filePaths) + let fs = require('fs') + var fsData = fs.readFileSync(result.filePaths[0]) + openAs.name = result.filePaths[0]; + let j = JSON.parse(fsData.toString()); + let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + if (type == 2) { + ipcRenderer.send('open-design-page', openAs.name, type); + return + } + let left = background_image.left; + let top = background_image.top; + let new_left = j.f.objects[0].left; + let new_top = j.f.objects[0].top; + for (let o of j.f.objects) { + o.left = o.left - new_left + left; + o.top = o.top - new_top + top; + } + // for (let o of j.b.objects) { + // o.left = o.left - new_left + left; + // o.top = o.top - new_top + top; + // } + canvas1.loadFromJSON(j.f); + canvas2.loadFromJSON(j.b); + background_image1 = canvas1.getObjects()[0]; + background_image2 = canvas2.getObjects()[0]; + + objs2 = j.bo; + objs1 = j.fo; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + background_image = background_image1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + background_image = background_image2; + step = step2; + } + canvas1.renderAll(); + canvas2.renderAll(); + updateList(); + // 修复打开新文件后无法撤销的bug + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j1.objects[0].hoverCursor = "default"; + recordJson1.push(j1); + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j2.objects[0].hoverCursor = "default"; + recordJson2.push(j2); + }, 0); + }).catch(err => { + console.log(err) + }) + } + }); + function clearAll() { + //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; + //let j = JSON.parse(blank); + //canvas1.setZoom(1); + //canvas2.setZoom(1); + //canvas1.loadFromJSON(j.f); + //canvas2.loadFromJSON(j.b); + let t_objs1 = canvas1.getObjects(); + let t_objs2 = canvas2.getObjects(); + for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]); + } + for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]); + } + objs2 = []; + objs1 = []; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + } + openAs.name = ""; + canvas1.renderAll(); + canvas2.renderAll(); + updateList(); + console.log(); + } + $("#new").click(function () { + if (step.val == 0) { + ipcRenderer.send('open-first-page'); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件? + output(() => ipcRenderer.send('open-first-page')); + return; + let j = canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let b = canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + left = 0, top = 0; + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let o = { f: j, b: {}, fo: objs1, bo: objs2, fp: fp, bp: '' }; + if (openAs.name != "") { + //打开的文件 + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page'); + //saveHistory(); + //clearAll(); + return; + } + dialog.showSaveDialog({ + title: language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result => { + if (result.filePath == "") { return; } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon'; + } + let fs = require('fs') + o.soonType = 1 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc") + openAs.name);//'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page'); + //saveHistory(); + //clearAll(); + }).catch(err => { + console.log() + }) + layer.close(index); + + }, function (index) { + /* + 2022-07-16 直接跳转至首页 + */ + //clearAll(); + ipcRenderer.send('open-first-page'); + }); + }); + function chagneTitle() { + + } + function addBackground() { + // 类型改变 + fabric.Image.fromURL('./public/images/bg_front_1.png', function (image) { + let width = $("#canvas1").width() + + image.left = ($("#canvas1").width() - image.width * image.get("scaleX")) / 2; + image.top = ($("#canvas1").height() - image.height * image.get("scaleY")) / 2; + + image.selectable = false; + image.hoverable = false; + image.hoverCursor = "default" + image.setSrc(image.toDataURL(), function (image) { + image.scale(1, 1); + background_image1 = image; + background_image = background_image1; + canvas.add(image); + canvas.renderAll(); + //pre_objs1.push(JSON.stringify(objs1)); + //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs1.push(JSON.stringify(objs1)); + recordJson1.push(canvas1.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"])); + }); + // 类型改变 + fabric.Image.fromURL('./public/images/bg_back.png', function (image) { + image.left = ($("#canvas2").width() - image.width) / 2; + image.top = ($("#canvas2").height() - image.height) / 2; + + image.selectable = false; + image.hoverable = false; + image.hoverCursor = "default" + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image; + canvas2.add(image); + canvas2.renderAll(); + //pre_objs2.push(JSON.stringify(objs2)); + //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs2.push(JSON.stringify(objs2)); + recordJson2.push(canvas2.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"])); + + let file = GetFile().get('file') + console.log(file) + if (file && file != 'empty') { + open(file) + } + }); + }); + is_bgi_add = true; + }); + } + function selectObject(target, index) { + updateControls(); + + $("#base_control").show(); + //$("#text_x").val((target.left - background_image.left).toFixed(1)); + //$("#text_y").val((target.top - background_image.top).toFixed(1)); + //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); + //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); + //$("#text_r").val(target.get("angle").toFixed(0)); + } + function updateControls() { + $("#text_r").val(canvas.getActiveObject().get("angle").toFixed(0)); + $("#text_x").val((canvas.getActiveObject().get("left") - background_image.left).toFixed(0)); + $("#text_y").val((canvas.getActiveObject().get("top") - background_image.top).toFixed(0)); + $("#text_h").val((canvas.getActiveObject().get("scaleY") * canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("scaleX") * canvas.getActiveObject().get("width")).toFixed(0)); + if (canvas.getActiveObject().get("selectable")) { + $("#lock_img").attr("src", "public/images/unlock20.png")//已经上锁,需要解锁 + } else { + $("#lock_img").attr("src", "public/images/lock20.png")//没上锁,可以上锁 + } + } + function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false; + } + } + for (let item of objs2) { + if (item.name == name) { + return false; + } + } + return true; + } + function resName(pre_name) { + //返回的是不重复的字段名 + let num = (objs1.length + objs2.length) - 1; + do { + num++; + } + while (!checkName(pre_name + num));//重复了就再加1 + return pre_name + num; + } + function addPic(pointer) { + + pre_add_image.left = pointer.x; + pre_add_image.top = pointer.y; + + canvas.add(pre_add_image); + + objs.push({ type: 1, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + //OpenDialog(); + //function OpenDialog() + //{ + // dialog.showOpenDialog({ + // title: "请选择图片", + // buttonLabel: "确认", + // filters: [ + // { name: 'Image File Type', extensions: ['png', 'jpg'] }, + // ] + // }).then(result => { + // if(result.canceled)return; + // //console.log(result.filePaths) + // fabric.Image.fromURL(result.filePaths[0], function(image) { + // image.left = pointer.x; + // image.top = pointer.y; + // + // canvas.add(image); + // objs.push({type: 1}); + // canvas.renderAll(); + // updateList(); + // recordState(); + // } ); + // }).catch(err => { + // console.log(err) + // }) + //} + } + function selectPic(target, index) { + //透明度 + + $("#pic_control").show(); + $("#set_bg").show(); + pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + + let idx = getIndex(canvas.getActiveObject()); + // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function addOutPic(pointer) { + fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + let width = canvas.getWidth(); + let height = canvas.getHeight(); + image.left = pointer.x; + image.top = pointer.y; + image.opacity = 1; + //image.on("selected",function(){//test well + // console.log(222); + //}); + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image); + objs.push({ type: 2, name: resName("OUT_PIC_"), black: false }); + canvas.renderAll(); + updateList(); + recordState(); + }); + }); + } + function selectOutPic(target, index) { + //字段名 + $("#out_pic_control").show(); + out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + let idx = getIndex(canvas.getActiveObject()); + $("#out_pic_field_name").val(objs[idx - 1].name); + // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + + function addText(pointer) { + let text = new fabric.Text('TEXT', {//源:文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + fontSize: 100, + underline: false, + linethrough: false, + fontFamily: 'Arial' + }); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 3, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectText() { + //console.log(canvas.getActiveObject().get("fontFamily")); + $("#text_control").show(); + $("#text_color").val(canvas.getActiveObject().get("fill")); + $("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#text_size").val(val); + $("#text_text").val(canvas.getActiveObject().get("text")); + $("#text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#text_underline").addClass("ui-radio-acitve") + } else { + $("#text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#text_bold").addClass("ui-radio-acitve") + } else { + $("#text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#text_italics").addClass("ui-radio-acitve") + } else { + $("#text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#text_linethrough").addClass("ui-radio-acitve") + } else { + $("#text_linethrough").removeClass("ui-radio-acitve") + } + $("#circle_text_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; + } + function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180/Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg + } + function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width/2,canvas.height/2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance*2, + fontSize: 100, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + // originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height/2 ? true : false + }); + // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) + // text.set({diameter: distance*2}) + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectCircleText() { + let idx = getIndex(canvas.getActiveObject()); + $("#circle_text_control").show(); + $("#circle_text_color").val(canvas.getActiveObject().get("fill")); + $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); + circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")-500); + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(canvas.getActiveObject().get("text")); + $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function addOutText(pointer) { + //MERGE_TEXT_1 + let text = new fabric.Text("MERGE TEXT", {//源:合成文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + fontStyle: "normal", + fontWeight: "normal", + fontSize: 100, + textBackgroundColor: "", + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }); + + //text.on({ + // 'scaling': function(e) { + // var obj = this, + // w = obj.width * obj.scaleX, + // h = obj.height * obj.scaleY; + // + // obj.set({ + // 'height' : h, + // 'width' : w, + // 'scaleX' : 1, + // 'scaleY' : 1 + // }); + // } + //}); + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + + canvas.add(text); + objs.push({ type: 4, name: resName("OUT_TEXT_"), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectOutText() { + //color + //size + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $("#out_text_control").show(); + $("#out_text_color").val(canvas.getActiveObject().get("fill")); + $("#out_text_text").val(canvas.getActiveObject().get("text")); + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#out_text_size").val(val); + $("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + let idx = getIndex(canvas.getActiveObject()); + $("#out_text_field_name").val(objs[idx - 1].name); + $("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + $("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : ''); + $("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : ''); + $("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : ''); + if (canvas.getActiveObject().get("underline") == true) { + $("#out_text_underline").addClass("ui-radio-acitve") + } else { + $("#out_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#out_text_bold").addClass("ui-radio-acitve") + } else { + $("#out_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#out_text_italics").addClass("ui-radio-acitve") + } else { + $("#out_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#out_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#out_text_linethrough").removeClass("ui-radio-acitve") + } + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 10, + width: 300, + height: 300 + }); + canvas.add(rect); + objs.push({ type: 5, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectRect() { + //color + $("#rect_control").show(); + $("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + $("#rect_radius").val(canvas.getActiveObject().get("rx")); + rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + let a = canvas.getActiveObject().getCoords(); + console.log(getCoordsMinX(a)); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 10, + radius: 150, + strokeUniform: true, + }); + canvas.add(circle); + objs.push({ type: 6, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectCircle() { + //border-color + //color + $("#circle_control").show(); + $("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill")); + $("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke")); + $("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth")); + circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100)); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addLine(pointer) { + let line = new fabric.Line([100, 200, 400, 200], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 10, + strokeDashArray: [0, 0], + }); + + line.setControlVisible("tl", false); + line.setControlVisible("tr", false); + line.setControlVisible("br", false); + line.setControlVisible("bl", false); + line.setControlVisible("mt", false); + line.setControlVisible("mb", false); + canvas.add(line); + objs.push({ type: 7, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectLine() { + //color + //weight + $("#line_control").show(); + + let idx = getIndex(canvas.getActiveObject()); + $("#line_color").val(canvas.getActiveObject().get("stroke")); + $("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]); + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")) + + + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + } + function addQrCode(pointer) { + + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }); + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x; + image.top = pointer.y; + image.scale(2.4, 2.4) + canvas.add(image); + objs.push({ type: 8, name: resName("QR_CODE_"), color: "#000000", val: "https://www.cardsoon.com", black: false }); + canvas.renderAll(); + updateList(); + recordState(); + }); + } + function selectQrCode() { + //字段名 + //background-color + //color + $("#qrcode_Controll").show(); + let idx = getIndex(canvas.getActiveObject()); + $("#qrcode_field_name").val(objs[idx - 1].name); + $("#qrcode_color").val(objs[idx - 1].color); + $("#qrcode_val").val(objs[idx - 1].val); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#barcode_controll").hide(); + } + function addBarCode(pointer) { + JsBarcode("#barcode", "123456789", { margin: 0, lineColor: "#000000", fontSize: 18, font: "Arial" }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x; + image.top = pointer.y; + image.scale(2.4, 2.4) + canvas.add(image); + objs.push({ type: 9, name: resName("BAR_CODE_"), val: "123456789", color: "#000000", fontSize: 18, font: "Arial", show: false, black: false });//show是取消文字显示 + canvas.renderAll(); + updateList(); + recordState(); + }); + } + function selectBarCode() { + //字段名 + //background-color + //color + $("#barcode_controll").show(); + let idx = getIndex(canvas.getActiveObject()); + $("#barcode_field_name").val(objs[idx - 1].name); + $("#barcode_font_family").val(objs[idx - 1].font); + let val = parseFloat(objs[idx - 1].fontSize)*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#barcode_size").val(val); + $("#barcode_val").val(objs[idx - 1].val); + $("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : ''); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + } + function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fontSize: 100, + fill: "#000000", + }); + canvas.add(text); + objs.push({ type: 10, name: resName("COUNTER_"), start: 0, add: 1, black: false }); + canvas.renderAll(); + updateList(); + recordState(); + console.log(canvas.getObjects()) + } + function selectCounter() { + //起始 + //终止 + //增量 + $("#counter_control").show(); + + + let idx = getIndex(canvas.getActiveObject()); + $("#counter_start").val(objs[idx - 1].start); + $("#counter_add").val(objs[idx - 1].add); + $("#counter_color").val(canvas.getActiveObject().get("fill")); + $("#count_font_family").val(canvas.getActiveObject().get("fontFamily")); + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#count_size").val(val); + + $("#line_control").hide(); + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function getIndex(target, _canvas = null) { + let temp_objs; + if (_canvas == null) { + temp_objs = canvas.getObjects(); + } else { + temp_objs = _canvas.getObjects(); + } + + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return i; + } + i++; + } + return 0; + } + function getType(target) { + let temp_objs = canvas.getObjects(); + let i = 0; + for (let res of temp_objs) { + if (target == res) { + return objs[i].type; + } + i++; + } + } + // =========================================================== + // 1. 刷新对象列表 (序号顺序:底层=1,顶层=N,修复:Flex布局防遮挡 + 序号靠右) + // =========================================================== + function updateList() { + let str = ""; + let i = 0; + + // 定义行样式:Flex布局,两端对齐,相对定位防止溢出 + const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;"; + // 定义左侧内容样式 + const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;"; + // 定义序号样式:靠右,有左边框 + const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;"; + + for (let item of objs) { + let displayIndex = i + 1; // 序号底层为1 + + // 图标逻辑 + let iconName = "rect.svg"; + switch (item.type) { + case 1: iconName = "images.svg"; break; + case 2: iconName = "images_add.svg"; break; + case 3: iconName = "text.svg"; break; + case 4: iconName = "text2.svg"; break; + case 5: iconName = "rect.svg"; break; + case 6: iconName = "round.svg"; break; + case 7: iconName = "line.svg"; break; + case 8: iconName = "qrcode.svg"; break; + case 9: iconName = "barcode.svg"; break; + case 10: iconName = "counter.svg"; break; + case 11: iconName = "text3.svg"; break; + } + + // 名称逻辑 + let nameText = item.name ? item.name : + (item.type === 3 ? "TEXT_" + displayIndex : + (item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex)); + + // 组装 HTML + str = `
    +
    + + ${nameText} +
    + ${displayIndex} +
    ` + str; + i++; + } + $(".obj_list").html(str); + + // 重新绑定点击事件(防止 updateList 后点击失效) + $(".obj_list > div").on("click", function () { + let imgId = $(this).find("img").attr("id"); + if(imgId) { + let idx = parseInt(imgId.replace("obj", "")); + selectObj(idx); + } + }); + } + + // =========================================================== + // 2. 列表选中高亮 (修复:自动滚动 + 防冲突) + // =========================================================== + function selectObj(objIndices, isFromCanvas = false) { + let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []); + + // 清除所有高亮 + $('.obj_list > div').css("background-color", "transparent"); + + // 遍历列表查找并高亮 + $('.obj_list > div').each(function () { + let $img = $(this).find("img"); + if ($img.length === 0) return; + + let idIndex = parseInt($img.attr("id").replace("obj", "")); + + if (indicesArray.includes(idIndex)) { + $(this).css("background-color", "#6F7A84"); + + // 自动滚动到可视区域 + if (isFromCanvas && indicesArray[0] === idIndex) { + this.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + } + }); + + // 如果是点击列表,触发画布选中 + if (!isFromCanvas && indicesArray.length === 1) { + let target = canvas.getObjects()[indicesArray[0] + 1]; + if (target) { + canvas.setActiveObject(target); + canvas.requestRenderAll(); + handleObjectSelected(); + } + } + } + function recordState() { + //pre_objs.push(JSON.stringify(objs)); + //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //next_json = []; + //next_objs = []; + //recordObjs.push(JSON.stringify(objs)); + //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + ++step.val; + for (let i = step.val; i < recordObjs.length; i++) { + recordObjs.pop(); + recordJson.pop(); + } + //recordObjs.splice(step.val, 1, JSON.stringify(objs)) + //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) + recordObjs.push(JSON.stringify(objs)) + j = canvas.toJSON(["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline"]); + j.objects[0].hoverCursor = "default"; + recordJson.push(j); + } + function getCoordsMinX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x; + } + function getCoordsMaxX(acoords) { + x = acoords[0].x; + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x; + + } + function getCoordsMinY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y; + + } + function getCoordsMaxY(acoords) { + y = acoords[0].y; + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y; + } + + $("#text_y").bind('input propertychange', function () { + if ($("#text_y").val() == "") { + $("#text_y").val(0) + } + let val = background_image.top; + if ($("#text_y").val() != "") { + val = background_image.top + parseInt($("#text_y").val()); + } + canvas.getActiveObject().set("top", val) + canvas.renderAll(); + recordState(); + }) + $("#text_x").bind('input propertychange', function () { + if ($("#text_x").val() == "") { + $("#text_x").val(0) + } + let val = background_image.left; + if ($("#text_x").val() != "") { + val = background_image.left + parseInt($("#text_x").val()); + } + canvas.getActiveObject().set("left", val) + canvas.renderAll(); + recordState(); + }) + $("#text_w").bind('input propertychange', function () { + let val = "0"; + if ($("#text_w").val() != "") { + val = $("#text_w").val(); + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("width", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleX", parseInt(val) / canvas.getActiveObject().get("width")) + } + + canvas.renderAll(); + recordState(); + }) + $("#text_h").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + if (objs[idx - 1].type == 7) { + if ($("#text_h").val() == "") { + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")); + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#text_h").val())); + canvas.renderAll(); + return; + } + let val = "0"; + if ($("#text_h").val() != "") { + val = $("#text_h").val(); + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("height", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleY", parseInt(val) / canvas.getActiveObject().get("height")) + } + + canvas.renderAll(); + recordState(); + }) + $("#text_r").bind('input propertychange', function () { + + if ($("#text_r").val() == "") { + $("#text_r").val("0") + } + $("#text_r").val(parseInt($("#text_r").val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($("#text_r").val())) + canvas.renderAll(); + recordState(); + }) + $("#out_pic_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_pic_field_name").val(); + updateList(); + recordState(); + }) + + $("#text_black").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].black = true; + } else { + objs[idx - 1].black = false; + } + recordState(); + }); + $("#out_text_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_text_field_name").val(); + updateList(); + recordState(); + }) + $("#qrcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#qrcode_field_name").val(); + updateList(); + recordState(); + }) + $("#qrcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64($("#qrcode_val").val(), { padding: 0, foreground: $(this).val() }); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#qrcode_val").bind('change', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#barcode_field_name").val(); + updateList(); + recordState(); + }) + $("#text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#text_back_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll(); + updateList(); + recordState(); + }) + $("#out_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#out_text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#out_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#out_text_back_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + // 监听合成文本输入框 (占位符) + $("#out_text_text").on('input propertychange', function () { + let val = $(this).val(); + + // 【核心修改】如果值为空,强制给 2 个空格 + // 这样宽度足够宽,容易点击选中 + if (val === "") { + val = " "; + } + + let obj = canvas.getActiveObject(); + if (obj) { + obj.set("text", val); + obj.setCoords(); + canvas.requestRenderAll(); + updateList(); + recordState(); + } + }); + $("#rect_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#rect_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#rect_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#rect_stroke").val()); + + canvas.renderAll(); + recordState(); + }) + $("#rect_stroke_width").bind('input propertychange', function () { + if ($("#rect_stroke_width").val() == "") { + $("#rect_stroke_width").val("0") + } else { + $("#rect_stroke_width").val(parseInt($("#rect_stroke_width").val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#rect_stroke_width").val())); + canvas.renderAll(); + recordState(); + }) + $("#rect_radius").bind('input propertychange', function () { + if ($("#rect_radius").val() == "") { + $("#rect_radius").val("0") + } else { + $("#rect_radius").val(parseInt($("#rect_radius").val())) + } + canvas.getActiveObject().set("rx", parseInt($("#rect_radius").val())); + canvas.getActiveObject().set("ry", parseInt($("#rect_radius").val())); + canvas.renderAll(); + recordState(); + }) + $("#circle_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#circle_stroke").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_stroke_width").bind('input propertychange', function () { + if ($("#circle_stroke_width").val() == "") { + $("#circle_stroke_width").val("0") + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#circle_stroke_width").val())); + canvas.renderAll(); + recordState(); + }) + $("#counter_start").bind('input propertychange', function () { + canvas.getActiveObject().set("text", $("#counter_start").val()); + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].start = $(this).val(); + canvas.renderAll(); + recordState(); + }) + $("#counter_add").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].add = $(this).val(); + canvas.renderAll(); + recordState(); + }) + $("#counter_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#counter_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $("#line_color").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#line_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#line_dist").bind('input propertychange', function () { + if ($("#line_dist").val() == "") { + $("#line_dist").val(0) + } + canvas.getActiveObject().set("strokeDashArray", [parseInt($("#line_dist").val()), parseInt($("#line_dist").val())]) + canvas.renderAll(); + recordState(); + }) + // 监听普通文本输入 (占位符) + $("#text_text").on('input propertychange', function () { + // 1. 获取输入框的值,不要用 trim(),否则无法输入空格 + let val = $(this).val(); + + // 2. 核心修改:只有当删光时,才给 4 个空格作为占位符 + if (val === "") { + val = " "; // 4个空格,比原来的2个更宽,更好点 + } + // 3. 更新对象 + // 注意:这里直接用 val,不再用 ` ${val} `,这样有文字时就不会强制加空格了 + canvas.getActiveObject().set("text", val); + canvas.requestRenderAll(); // 使用 requestRenderAll 性能更好 + updateList(); + recordState(); + }); + $("#barcode_val").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].val = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }) + $("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val() + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + val = 0 + } else { + objs[idx - 1].show = false; + } + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val*dpi/72, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val*dpi/72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }) + $('#count_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + $('#text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + + $('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + + $('#language_select').on('change', function () {//语言选择 + langua_ge($(this).find('option:selected').val()); + s_lan = $(this).find('option:selected').val(); + localStorage.setItem("lang", s_lan); + }); + $('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].font = $(this).find('option:selected').text(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + //recordState(); + }); + $('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + $(".obj_list").on("click", "div", function () { + let idx = objs.length - 1 - $(this).index(); + selectObj(idx); + }); + $("#exceed").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].exceed = true; + } else { + objs[idx - 1].exceed = false; + } + recordState(); + }); + $("#autoWrap").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].autoWrap = true; + } else { + objs[idx - 1].autoWrap = false; + } + recordState(); + }); + $("#fixSize").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].fixSize = true; + } else { + objs[idx - 1].fixSize = false; + } + recordState(); + }); + $("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + recordState(); + }); + $("#delete").click(function () { + let actObjs = canvas.getActiveObjects(); + let allObjs = canvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1); + canvas.remove(item); + flag = 1; + + } + } + updateList(); + canvas.discardActiveObject(); + if (flag == 1) { + recordState(); + canvas.renderAll(); + } + // let actObjs = canvas.getActiveObjects(); + // let allObjs = canvas.getObjects(); + // let flag = 0; + // for( let item of actObjs){ + // if ( item != allObjs[0]){ + // let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + // objs.splice(i-1,1); + // canvas.remove(item); + // flag = 1; + // updateList(); + // } + // } + // selectObj(-1); + // if(flag == 1){ + // recordState(); + // } + }); + + //在关于我们页面,获取版本号。 + // 注意:function 前面加了 async 关键字 + $("#about").click(async function () { + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + console.error("获取版本号失败", e); + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
    ' + + '
    ' + + '
    Soon Design
    ' + + // 【修改点】这里使用了刚才获取到的 version 变量 + '
    ' + language_str("vers") + ' ' + version + '
    ' + + '
    ' + language_str("copyright") + '
    ' + + '
    ' + }); + }) + $("#help").click(() => { + ipcRenderer.send('open-help-file'); + }); + + + // =========================================================== + // 对象选中处理 + // =========================================================== + function handleObjectSelected() { + let select_obj = canvas.getActiveObject(); + + // 1. 未选中任何对象 + if (!select_obj) { + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + $("#component_type").text(language_str("bg")); + selectObj(-1, true); // 传 true,只更新UI + return; + } + + // 2. 获取索引 + let i = getIndex(select_obj); + + if (i != 0) { + // --- 选中了普通对象 --- + --i; // 转换为 objs 数组下标 + + if(objs[i]) { + switch (objs[i].type) { + case 1: // 图片 + $("#component_type").text(language_str("img")); + selectObject(select_obj, i); selectPic(select_obj, i); break; + case 2: // 合成图片 + $("#component_type").text(language_str("simg")); + selectObject(select_obj, i); selectOutPic(select_obj, i); break; + case 3: // 文本 + $("#component_type").text(language_str("text")); + selectObject(select_obj, i); selectText(select_obj, i); break; + case 4: // 合成文本 + $("#component_type").text(language_str("stext")); + selectObject(select_obj, i); selectOutText(select_obj, i); break; + case 5: // 矩形 + $("#component_type").text(language_str("rect")); + selectObject(select_obj, i); selectRect(select_obj, i); break; + case 6: // 圆形 + $("#component_type").text(language_str("circ")); + selectObject(select_obj, i); selectCircle(select_obj, i); break; + case 7: // 直线 + $("#component_type").text(language_str("line")); + selectObject(select_obj, i); selectLine(select_obj, i); break; + case 8: // 二维码 + $("#component_type").text(language_str("qrc")); + selectObject(select_obj, i); selectQrCode(select_obj, i); break; + case 9: // 条形码 + $("#component_type").text(language_str("barc")); + selectObject(select_obj, i); selectBarCode(select_obj, i); break; + case 10: // 计数器 + $("#component_type").text(language_str("counter")); + selectObject(select_obj, i); selectCounter(select_obj, i); break; + case 11: // 圆形文本 + $("#component_type").text(language_str("ctext")); + selectObject(select_obj, i); selectCircleText(select_obj, i); break; + } + } + + // 【关键】这里传入 true! + // 意思是:因为我已经是在画布上选中它了,selectObj 函数只负责把左边的条目变灰,不要再回头去 setActiveObject 了 + selectObj(i, true); + + } else { + // --- 选中了背景 --- + $("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide(); + selectObj(-1, true); + $("#component_type").text(language_str("bg")); + } + } + + // =========================================================== + // 统一事件监听 (替代原有的 canvas1.on 和 canvas2.on) + // =========================================================== + // 提取对象移动逻辑 (供正反面复用) + function handleObjectMoving(e) { + let obj = e.target; + let idx = getIndex(obj); + // 圆形文本特殊处理 + if (idx > 0 && idx <= objs.length && objs[idx - 1] && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { x: obj.left, y: obj.top }; + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2); + let angleDeg = getDeg(pointer); + let flipped = pointer.y > canvas.height / 2 ? true : false; + + if(typeof circle_text_diameter !== 'undefined') circle_text_diameter.setValue(distance * 2 - 500); + obj.set("diameter", distance * 2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); + } + + // 启动监听 + // =========================================================== + // 统一事件绑定 (已集成边界检测) + // =========================================================== +// =========================================================== + // 3. 统一事件绑定 (修复:使用 Global Composite Operation 实现完美遮罩) + // =========================================================== + function initCanvasEvents() { + [canvas1, canvas2].forEach(currentCanvas => { + + // 1. 鼠标移动 + currentCanvas.on("mouse:move", (e) => { + let offsetX = 0, offsetY = 0; + if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { + offsetX = background_image.left; + offsetY = background_image.top; + } + if(e.pointer) { + $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); + } + updateRulerGuides(e.e); + }); + + // 2. 鼠标移出 + currentCanvas.on("mouse:out", () => { + $("#ruler-guide-h").hide(); $("#ruler-guide-v").hide(); + }); + + // 3. 选中处理 + currentCanvas.on('mouse:down', handleObjectSelected); + + // 4. 鼠标松开 (添加对象) + currentCanvas.on("mouse:up", (e) => { + if (addState === 0) return; + const ptr = e.pointer; + switch (addState) { + case 1: addPic(ptr); break; + case 2: addOutPic(ptr); break; + case 3: addText(ptr); break; + case 4: addOutText(ptr); break; + case 5: addRect(ptr); break; + case 6: addCircle(ptr); break; + case 7: addLine(ptr); break; + case 8: addQrCode(ptr); break; + case 9: addBarCode(ptr); break; + case 10: addCounter(ptr); break; + case 11: addCircleText(ptr); break; + } + addState = 0; + if(typeof background_image !== 'undefined') background_image.hoverCursor = "default"; + currentCanvas.renderAll(); + $("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", ""); + }); + + // 5. 对象移动 (只保留圆形文本逻辑,去掉了 checkOutOfBounds) + currentCanvas.on("object:moving", (e) => { + handleObjectMoving(e); + }); + + // 6. 变换事件 + currentCanvas.on("object:rotating", updateControls); + currentCanvas.on("object:scaling", updateControls); + currentCanvas.on("object:rotated", () => { recordState(); }); + currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); + currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); + currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); }); + + // 7. 对象修改后 + currentCanvas.on('object:modified', function (event) { + if (currentCanvas.getActiveObjects().length > 1) return; + let idx = getIndex(event.target); + if(idx <= 0) return; + let objData = objs[idx - 1]; + let target = event.target; + + if ([3, 4, 10].includes(objData.type)) { + target.fontSize = (target.fontSize * target.scaleX).toFixed(0); + target.scaleX = 1; target.scaleY = 1; + target._clearCache(); + if(objData.type == 3) $("#text_size").val(target.fontSize); + if(objData.type == 4) $("#out_text_size").val(target.fontSize); + if(objData.type == 10) $("#count_size").val(target.fontSize); + currentCanvas.renderAll(); + updateControls(); + } else if (objData.type == 5) { + target.width *= target.scaleX; target.height *= target.scaleY; + target.scaleX = 1; target.scaleY = 1; + updateControls(); + } + recordState(); + }); + // ================================================== + // 8. 【完美修复版】渲染后绘制:外部蒙版 + 中心圆孔 (修复三角形Bug) + // ================================================== + currentCanvas.on('after:render', function () { + let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2; + + if (bgObj) { + const ctx = currentCanvas.getContext(); + const width = currentCanvas.width; + const height = currentCanvas.height; + + // 1. 计算圆心 + const cx = bgObj.left + (bgObj.width * bgObj.scaleX) / 2; + const cy = bgObj.top + (bgObj.height * bgObj.scaleY) / 2; + + // 2. 计算半径 + // 外半径 (光盘大小) + const r_outer = (bgObj.width * bgObj.scaleX) / 2; + + // 内半径 (中心孔) + const dpi = 600; + const mmToPx = dpi / 25.4; + let holeDiameterMm = 24; // 默认 15mm + + // 切换版本判断 (15mm vs 38mm) + if (currentCanvas === canvas1 && typeof bg_version !== 'undefined' && bg_version === 2) { + holeDiameterMm = 42; + } + + // 比例计算法:确保孔径随缩放适配 + // r_inner = r_outer * (孔径 / 光盘标准直径120mm) + const r_inner = r_outer * (holeDiameterMm / 120); + + ctx.save(); + ctx.beginPath(); + + // A. 绘制全屏矩形 (顺时针) + ctx.rect(0, 0, width, height); + + // B. 挖掉外圆 (逆时针) + // 【关键修复】画圆前,必须把笔移动到圆的起点 (cx + r, cy) + // 否则 Canvas 会画一条线连过来,形成奇怪的三角形 + ctx.moveTo(cx + r_outer, cy); + ctx.arc(cx, cy, r_outer, 0, Math.PI * 2, true); + + // C. 填补内圆 (顺时针) + // 【关键修复】同理,把笔移动到内圆的起点 + ctx.moveTo(cx + r_inner, cy); + ctx.arc(cx, cy, r_inner, 0, Math.PI * 2, false); + + ctx.closePath(); + ctx.fillStyle = "rgba(0, 0, 0, 0.9)"; // 90% 遮罩 + ctx.fill(); + ctx.restore(); + } + }); + }); + } + // 立即启动 + initCanvasEvents(); + // =========================================================== + // Slider 滑块初始化 (保持原样) + // =========================================================== + var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, max: 120, + change: function (value) { + canvas.getActiveObject().set("fontSize", value); + canvas.renderAll(); + } + }); + var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 500, max: 4000, + change: function (value) { + canvas.getActiveObject().set("diameter", value); + canvas.renderAll(); + } + }); + var out_pic_op = slider.render({ + elem: '#out_pic_op', + change: function (value) { + canvas.getActiveObject().set('opacity', (100 - value) / 100); + canvas.renderAll(); + } + }); + var pic_op = slider.render({ + elem: '#pic_op', + change: function (value) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + }); + var rect_op = slider.render({ + elem: '#rect_op', + change: function (value) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + }); + var circle_op = slider.render({ + elem: '#circle_op', + change: function (value) { + canvas.getActiveObject().set("opacity", (100 - value) / 100); + canvas.renderAll(); + } + }); +// =========================================================== + // 3. 标尺功能 (0点复位 + 拖拽辅助线 + 透明度) + // =========================================================== + function initRulers() { + const dpi = 600; + const mmToPx = dpi / 25.4; + const rulerHeight = 20; + const rulerWidth = 20; + + const canvasWrapper = $('.canvas-wrapper'); + const canvasBox = $('.canvas_box'); + + // 确保容器定位 + if(canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative'); + + // 初始化尺寸 + let canvasWidth = canvasWrapper.width() || 800; + let canvasHeight = canvasWrapper.height() || 600; + + const $rulerH = $('#ruler-h'); + const $rulerV = $('#ruler-v'); + + $rulerH.attr('width', canvasWidth).attr('height', rulerHeight); + $rulerV.attr('width', rulerWidth).attr('height', canvasHeight); + + // --- 1. 绑定拖拽事件 (核心新功能) --- + // 定义临时拖拽线 DOM + let $dragLine = null; + + // 通用拖拽开始函数 + const startDragGuide = (type, e) => { + e.preventDefault(); + e.stopPropagation(); + + // 创建临时显示的线 + $dragLine = $('
    '); + $('.canvas-wrapper').append($dragLine); + + if (type === 'horizontal') { + // 从上往下拖 (水平线) + $dragLine.css({ + left: 0, width: '100%', height: '1px', top: e.offsetY + }); + } else { + // 从左往右拖 (垂直线) + $dragLine.css({ + top: 0, height: '100%', width: '1px', left: e.offsetX + }); + } + + // 绑定 document 移动事件 + $(document).on('mousemove.ruler', function(moveEvent) { + let wrapperOffset = canvasWrapper.offset(); + if (type === 'horizontal') { + let relY = moveEvent.pageY - wrapperOffset.top; + $dragLine.css('top', relY + 'px'); + } else { + let relX = moveEvent.pageX - wrapperOffset.left; + $dragLine.css('left', relX + 'px'); + } + }); + + // 绑定松开事件 (创建真实的 Fabric 线) + $(document).on('mouseup.ruler', function(upEvent) { + $(document).off('mousemove.ruler mouseup.ruler'); + + if (!$dragLine) return; + + // 获取最终位置 + let wrapperOffset = canvasWrapper.offset(); + let finalX = upEvent.pageX - wrapperOffset.left; + let finalY = upEvent.pageY - wrapperOffset.top; + + // 移除临时线 + $dragLine.remove(); + $dragLine = null; + + // 只有拖入画布区域才创建 + if (finalX > rulerWidth && finalY > rulerHeight) { + createFabricGuide(type, finalX, finalY); + } + }); + }; + + // 绑定到标尺 DOM + $rulerH.off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e)); + $rulerV.off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e)); + + // --- 2. 在画布中创建辅助线 --- + function createFabricGuide(type, domX, domY) { + // 计算画布内的逻辑坐标 (减去 margin 偏移,除以缩放) + // 注意:这里我们重新回到“原点在画布0,0”的逻辑 + // canvasBox.offset() 是画布容器的绝对位置 + let boxOffset = canvasBox.offset(); + let wrapperOffset = canvasWrapper.offset(); + + // 计算相对于 canvasBox 的像素位置 + let relX = domX - (boxOffset.left - wrapperOffset.left); + let relY = domY - (boxOffset.top - wrapperOffset.top); + + // 转为 Fabric 坐标 (除以 zoom) + let canvasX = relX / zoom; + let canvasY = relY / zoom; + + // 构造线对象 + let linePoints = []; + if (type === 'horizontal') { + // 水平线:贯穿整个画布宽度 + // 为了让线足够长,我们设一个很大的值,或者设为 canvas.width + linePoints = [-5000, canvasY, 5000, canvasY]; + } else { + // 垂直线 + linePoints = [canvasX, -5000, canvasX, 5000]; + } + + let guideLine = new fabric.Line(linePoints, { + stroke: '#00FFFF', // 青色高亮 + strokeWidth: 1 / zoom, // 线条粗细随缩放调整,保持视觉一致 + strokeDashArray: [5, 5], // 虚线 + selectable: true, // 允许选中移动 + evented: true, + hasControls: false, // 没有缩放控制点 + hasBorders: false, + lockRotation: true, // 锁定旋转 + lockScalingX: true, + lockScalingY: true, + hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize', + // 自定义属性标记 + isGuideLine: true, + ignoreSave: true // 保存时可以根据这个标记过滤掉 + }); + + // 限制移动方向 + guideLine.on('moving', function(e) { + if (type === 'horizontal') { + this.left = -5000; // 锁定 X 轴不许动 + } else { + this.top = -5000; // 锁定 Y 轴不许动 + } + }); + + canvas.add(guideLine); + canvas.setActiveObject(guideLine); + canvas.renderAll(); + } + + + // --- 3. 绘制标尺 (核心绘制循环) --- + function updateAndDraw() { + const pxPerMm = zoom * mmToPx; + + // 【修改点】原点复位逻辑 + // 现在原点就是 Canvas 的 (0,0) 点 + // 加上 canvasBox 的 margin 偏移量,让标尺 0 刻度对齐画布边缘 + let boxMarginLeft = parseFloat(canvasBox.css('margin-left')) || 0; + let boxMarginTop = parseFloat(canvasBox.css('margin-top')) || 0; + + let originX = boxMarginLeft; + let originY = boxMarginTop; + + const ctxH = document.getElementById('ruler-h').getContext('2d'); + const ctxV = document.getElementById('ruler-v').getContext('2d'); + + const drawAxis = (ctx, isH) => { + let len = isH ? canvasWidth : canvasHeight; + ctx.clearRect(0, 0, isH ? len : rulerWidth, isH ? rulerHeight : len); + + // 背景改为透明 (配合 CSS) + // ctx.fillStyle = 'rgba(44, 44, 44, 0.7)'; // 也可以在这里画 + // ctx.fillRect(...) + // 这里我们留空,让 CSS 背景色透出来 + + ctx.strokeStyle = '#999'; // 刻度颜色稍亮一点 + ctx.lineWidth = 1; + ctx.beginPath(); + + // 绘制底边线 + if(isH) { ctx.moveTo(0, 19); ctx.lineTo(len, 19); } + else { ctx.moveTo(19, 0); ctx.lineTo(19, len); } + ctx.stroke(); + + ctx.fillStyle = '#CCC'; + ctx.font = '9px Arial'; + ctx.textAlign = isH ? 'left' : 'center'; + ctx.textBaseline = isH ? 'top' : 'middle'; + ctx.beginPath(); + + // 绘制刻度 (只向正方向绘制) + // 既然原点复位到0了,我们只需要从0开始画 + for (let mm = 0; ; mm += 1) { + let pos = (isH ? originX : originY) + (mm * pxPerMm); + + if (pos > len) break; + + let isMajor = mm % 10 === 0; // 1cm + let isMedium = mm % 5 === 0; // 0.5cm + let tickLen = isMajor ? 15 : (isMedium ? 8 : 4); + + if(isH) { + ctx.moveTo(pos, 20 - tickLen); ctx.lineTo(pos, 20); + if(isMajor) ctx.fillText(mm/10, pos + 2, 2); // 显示 cm 数值 + } else { + ctx.moveTo(20 - tickLen, pos); ctx.lineTo(20, pos); + if(isMajor) { + ctx.save(); + ctx.translate(8, pos + 2); + ctx.rotate(-Math.PI/2); + ctx.fillText(mm/10, 0, 0); + ctx.restore(); + } + } + } + ctx.stroke(); + }; + + drawAxis(ctxH, true); + drawAxis(ctxV, false); + } + + canvas1.on('after:render', updateAndDraw); + canvas2.on('after:render', updateAndDraw); + $(window).resize(() => setTimeout(updateAndDraw, 100)); + updateAndDraw(); + } + + // 辅助函数:更新光标位置 + function updateRulerGuides(nativeEvent) { + if (!nativeEvent) return; + const wrapper = $('.canvas-wrapper'); + const offset = wrapper.offset(); + if (!offset) return; + $("#ruler-guide-h").css({ left: (nativeEvent.pageX - offset.left) + 'px', display: 'block' }); + $("#ruler-guide-v").css({ top: (nativeEvent.pageY - offset.top) + 'px', display: 'block' }); + } + + + + + }) + }); \ No newline at end of file diff --git a/lib/design1.js b/frontend-web/js/design1.js similarity index 98% rename from lib/design1.js rename to frontend-web/js/design1.js index 6be4ac2..728df6f 100644 --- a/lib/design1.js +++ b/frontend-web/js/design1.js @@ -49,6 +49,7 @@ if (ipcRenderer) { var $ = layui.$; var layer = layui.layer; + layer.config({ skin: 'soon-layer' }); var slider = layui.slider; var form = layui.form; var colorpicker = layui.colorpicker; @@ -505,9 +506,9 @@ if (ipcRenderer) { s.onload = s.onerror = function () { if (typeof next === 'function') next(); }; document.body.appendChild(s); } - loadScript('./lib/design1/output.js', function () { - loadScript('./lib/design1/core.js', function () { - loadScript('./lib/design1/ui.js', function () {}); + loadScript('../js/design1/output.js', function () { + loadScript('../js/design1/core.js', function () { + loadScript('../js/design1/ui.js', function () {}); }); }); return; @@ -515,7 +516,7 @@ if (ipcRenderer) { const loadModule = (moduleName) => { if (!path || !fs) return; - const jsPath = path.join(appPath, 'lib', 'design1', moduleName + '.js'); + const jsPath = path.join(appPath, 'js', 'design1', moduleName + '.js'); if (fs.existsSync(jsPath)) { try { eval(fs.readFileSync(jsPath, 'utf8')); diff --git a/lib/design1/core.js b/frontend-web/js/design1/core.js similarity index 98% rename from lib/design1/core.js rename to frontend-web/js/design1/core.js index 0a6939d..10fc9de 100644 --- a/lib/design1/core.js +++ b/frontend-web/js/design1/core.js @@ -375,7 +375,7 @@ window.rotate = function (id) { function addBackground() { // 类型改变 - fabric.Image.fromURL('./public/images/bg_front_1.png', function (image) { + fabric.Image.fromURL(soonAsset('bg_front_1.png'), function (image) { let width = $("#canvas1").width() image.left = ($("#canvas1").width() - image.width * image.get("scaleX")) / 2; @@ -399,7 +399,7 @@ function addBackground() { recordJson1.push(canvas1.toJSON(TO_JSON_PROPERTIES)); }); // 类型改变 - fabric.Image.fromURL('./public/images/bg_back.png', function (image) { + fabric.Image.fromURL(soonAsset('bg_back.png'), function (image) { image.left = ($("#canvas2").width() - image.width) / 2; image.top = ($("#canvas2").height() - image.height) / 2; @@ -429,6 +429,15 @@ function addBackground() { } catch (e) {} } if (file && file != 'empty') { + if (file.indexOf('soondesign_file:') === 0 && typeof sessionStorage !== 'undefined') { + try { + var metaRaw = sessionStorage.getItem('soondesign_open_meta'); + if (metaRaw) { + window._soonFileMeta = JSON.parse(metaRaw); + sessionStorage.removeItem('soondesign_open_meta'); + } + } catch (e) {} + } var p = window.openFile(file) if (p && p.then) p.then(function() {}, function() {}) } @@ -451,9 +460,9 @@ window.updateControls = function updateControls() { $("#text_w").val((currentCanvas.getActiveObject().get("scaleX") * currentCanvas.getActiveObject().get("width")).toFixed(0)); // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) if (currentCanvas.getActiveObject().get("lockMovementX")) { - $("#lock_img").attr("src", "public/images/lock20.png")//已锁定 + $("#lock_img").attr("src", soonAsset("lock20.png"))//已锁定 } else { - $("#lock_img").attr("src", "public/images/unlock20.png")//未锁定 + $("#lock_img").attr("src", soonAsset("unlock20.png"))//未锁定 } }; // 向后兼容 @@ -540,7 +549,7 @@ function selectPic(target, index) { } function addOutPic(pointer) { - fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) { let width = canvas.getWidth(); let height = canvas.getHeight(); image.left = pointer.x; @@ -1155,14 +1164,13 @@ window.updateList = function updateList() { } // 锁定状态图标 - const lockIcon = isLocked ? - '' : - ''; + const lockIcon = isLocked ? + `` : + ``; - // 组装 HTML str = `
    - + ${nameText} ${lockIcon}
    diff --git a/frontend-web/js/design1/output.js b/frontend-web/js/design1/output.js new file mode 100644 index 0000000..2bad78b --- /dev/null +++ b/frontend-web/js/design1/output.js @@ -0,0 +1,1490 @@ +// openAs 对象用于跟踪当前打开的文件路径 +var openAs = { + _name: "", + set name(val) { + if (val == "") { + $("title").html('Soon Design'); + } else { + var titleName = (typeof window.soonFormatOpenTitle === 'function') ? window.soonFormatOpenTitle(val) : val; + $("title").html('Soon Design - ' + titleName); + } + this._name = val; + }, + get name() { + return this._name; + } +}; + +// 导出文件(兼容桌面 fs 与网页 bridge 下载) +async function saveImageAsPNG(buffer) { + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); + if (!dialogApi) return; + var pathJoin = (path && path.join) ? path.join.bind(path) : function() { return [].slice.call(arguments).join('/').replace(/\/+/g, '/'); }; + var defaultPath = exePath ? pathJoin(exePath, 'output.png') : 'output.png'; + const result = await dialogApi.showSaveDialog({ + title: '保存图片', + defaultPath: defaultPath, + filters: [{ name: 'PNG Images', extensions: ['png'] }] + }); + var canceled = result.canceled, filePath = result.filePath || result.fileHandle; + if (canceled || !filePath) return; + if (window.platformBridge && window.platformBridge.writeFile) { + var blob = buffer instanceof Uint8Array ? new Blob([buffer]) : (buffer && buffer.buffer ? new Blob([buffer]) : new Blob([buffer])); + var name = (typeof filePath === 'string' ? filePath.split(/[/\\]/).pop() : 'output.png') || 'output.png'; + if (!/\.png$/i.test(name)) name = name + '.png'; + if (result.fileHandle && result.fileHandle.createWritable) { + result.fileHandle.createWritable().then(function(w) { w.write(blob); return w.close(); }).then(function() { layer.msg(language_str("saveSucc") + name); }); + } else { + var a = document.createElement('a'); + a.download = name; + a.href = URL.createObjectURL(blob); + a.click(); + URL.revokeObjectURL(a.href); + layer.msg(language_str("saveSucc") + name); + } + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFile(filePath, buffer, (err) => { + if (!err) layer.msg(language_str("saveSucc") + filePath); + }); + } +} + +// 将 display_func 附加到 window 对象,确保全局可访问 +window.display_func = function display_func(img1, img2, img3) { + if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return; + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + + // 保存所有对象的 selectable 和 evented 状态,确保预览后能恢复 + const objStates1 = []; + const objStates2 = []; + canvas1.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine) { + objStates1[index] = { + selectable: obj.selectable, + evented: obj.evented + }; + } + }); + canvas2.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine) { + objStates2[index] = { + selectable: obj.selectable, + evented: obj.evented + }; + } + }); + + // 过滤掉辅助线 + let _objs1 = canvas1.getObjects().filter(obj => !obj.isGuideLine); + let g1 = []; + let g3 = []; + let g5 = []; + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + // 过滤掉辅助线 + let list1 = _objs1.filter(obj => !obj.isGuideLine); + for (let item of list1) { + let idx = getIndex(item, canvas1); + + // 安全检查:确保 objs1[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs1[idx - 1] || typeof objs1[idx - 1].type === 'undefined')) { + continue; + } + + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + // 过滤掉辅助线 + let _objs2 = canvas2.getObjects().filter(obj => !obj.isGuideLine); + let g2 = []; + let g4 = []; + let g6 = []; + left = 0; + top = 0; + black_top = 0; + black_left = 0; + all_top = 0; + all_left = 0; + for (let item of _objs2) { + + let idx = getIndex(item, canvas2); + + // 安全检查:确保 objs2[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs2[idx - 1] || typeof objs2[idx - 1].type === 'undefined')) { + continue; + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//黑色字或计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && (objs2[idx - 1].type == 10)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + var printPath, printBlobUrl, buffer; + if (typeof require !== 'undefined' && require('fs')) { + var fs = require('fs'); + var Buffer = require('buffer').Buffer; + var buf = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + printPath = path.join(exePath, 'print.png'); + fs.writeFileSync(printPath, buf); + buffer = buf; + } else { + var bin = atob(url3.replace(/^data:image\/\w+;base64,/, "")); + var arr = new Uint8Array(bin.length); + for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + buffer = arr; + printBlobUrl = URL.createObjectURL(new Blob([arr], { type: 'image/png' })); + printPath = printBlobUrl; + } + + layer.open({ + type: 1, + area: [w, h], + title: language_str("display"), //不显示标题栏"预览" + shadeClose: true, //点击遮罩关闭 + content: ` +
    +
    +
    + +
    + +
    +
    +
    +
    `, + btn: [language_str("output"), "打印"],//'导出' + btn1: function (index, layero) { + saveImageAsPNG(buffer); + }, + btn2: function () { + if (window.platformBridge && window.platformBridge.printPdf) { + var blob = buffer instanceof Uint8Array ? new Blob([buffer], { type: 'image/png' }) : new Blob([buffer], { type: 'image/png' }); + window.platformBridge.printPdf(blob); + } else if (typeof printJS === 'function') { + printJS({ printable: url3, type: 'image', style: 'img { width: 100%; height: auto; }' }); + } + }, + end: function () { + if (printBlobUrl) try { URL.revokeObjectURL(printBlobUrl); } catch (e) {} + // 预览窗口关闭后,恢复所有对象的 selectable 和 evented 状态 + canvas1.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine && objStates1[index]) { + obj.set({ + selectable: objStates1[index].selectable, + evented: objStates1[index].evented + }); + } + }); + canvas2.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine && objStates2[index]) { + obj.set({ + selectable: objStates2[index].selectable, + evented: objStates2[index].evented + }); + } + }); + canvas1.renderAll(); + canvas2.renderAll(); + } + }); +} + +// 将 output 附加到 window 对象,确保全局可访问 +window.output = function output(callback = null, _save = save) { + if (typeof window.soonGuardMemberExport === 'function' && !window.soonGuardMemberExport()) return; + // 类型改变 + fabric.Image.fromURL(soonAsset('op_1.png'), function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + fabric.Image.fromURL(soonAsset('op_1.png'), function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + + $("#base_control").hide(); + $("#line_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" + canvas1.discardActiveObject().renderAll(); + canvas2.discardActiveObject().renderAll(); + let _objs1 = canvas1.getObjects(); + let g1 = []; + let g3 = []; + let g5 = [];//黑色图层 + let left = 0;//左裁剪 + let top = 0;//顶裁剪 + let black_left = 0;//黑色左裁剪 + let black_top = 0;//黑色顶裁剪 + let all_left = 0;//预览图左裁剪 + let all_top = 0;//预览图顶裁剪 + // 过滤掉辅助线 + let filteredObjs1 = _objs1.filter(obj => !obj.isGuideLine); + for (let item of filteredObjs1) { + let idx = getIndex(item, canvas1); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g3.push(fabric.util.object.clone(item)); + + // 安全检查:确保 objs1[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs1[idx - 1] || typeof objs1[idx - 1].type === 'undefined')) { + continue; + } + + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs1[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs1[idx - 1].type == 3 + && + item.get("fill") == "#000000" + //文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 11 + && + item.get("fill") == "#000000" + //圆形文字 && 黑色 + ) + || + ( + objs1[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs1[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs1[idx - 1].black == true //勾选并入黑色图层 + + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g1.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g1.push(fabric.util.object.clone(item)); + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + g3.push(img1); + let url3 = new fabric.Group(g3).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url5 = new fabric.Group(g5).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + let _objs2 = canvas2.getObjects(); + let g2 = []; + let g4 = []; + let g6 = [];//黑色图层 + left = 0; + top = 0; + black_left = 0;//黑色左裁剪 + black_top = 0;//黑色顶裁剪 + all_left = 0;//黑色左裁剪 + all_top = 0;//黑色顶裁剪 + // 过滤掉辅助线 + let filteredObjs2 = _objs2.filter(obj => !obj.isGuideLine); + for (let item of filteredObjs2) { + let idx = getIndex(item, canvas2); + + // 安全检查:确保 objs2[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs2[idx - 1] || typeof objs2[idx - 1].type === 'undefined')) { + continue; + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g4.push(fabric.util.object.clone(item)); + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue;//计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue;//二维码 条形码 + } + if (idx != 0 && (objs2[idx - 1].type == 2)) { + continue;//合成图片 + } + if (idx != 0 && + ( + ( + objs2[idx - 1].type == 3 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) + || + ( + objs2[idx - 1].type == 5//矩形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 6//圆形 + && + ( + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("stroke") == "") //黑填充 透明边框 + || + (item.get("fill") == "" && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //透明填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff")) //黑填充 黑边框 + || + ((item.get("fill") == "#000000" || item.get("fill") == "#ffffff") && item.get("strokeWidth") == 0) //黑填充 0边框 + ) + ) + || + ( + objs2[idx - 1].type == 7//直线 + && + (item.get("stroke") == "#000000" || item.get("stroke") == "#ffffff") + ) + || + objs2[idx - 1].black == true //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + continue;//黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g2.push(fabric.util.object.clone(item)); + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + g2.push(fabric.util.object.clone(item)); + + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 2787, + width: 2787, + top: top, + left: left + }); + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 2787, + width: 2787, + }); + g4.push(img2); + let url4 = new fabric.Group(g4).toDataURL({ + height: 2787, + width: 2787, + top: all_top, + left: all_left + }); + let url6 = new fabric.Group(g6).toDataURL({ + height: 2787, + width: 2787, + top: black_top, + left: black_left + }); + // let white_url2 = canvas2.getObjects()[0].toDataURL({ + // height: 2787, + // width: 2787, + // }); + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + "px"; + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + "px"; + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + "px"; + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + "px"; + h1 = document.body.clientHeight * 0.8 - 150 + "px"; + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: "", backColorPic: "", frontBlackPic: "", backBlackPic: "", frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: '' }; + if (objs1.length != 0 & objs2.length == 0) { + op.flag = 2; + } else if (objs1.length == 0 & objs2.length != 0) { + op.flag = 3; + } + op.frontColorPic = desEncrypt(url1); + op.backColorPic = ''; + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = ''; + op.backBlackPic = ''; + for (let item of _objs1) { + // 跳过辅助线 + if (item.isGuideLine) { + continue; + } + + let idx = getIndex(item, canvas1); + if (idx == 0) continue;//background-image + + // 安全检查:确保 objs1[idx - 1] 存在且有 type 属性 + if (!objs1[idx - 1] || typeof objs1[idx - 1].type === 'undefined') { + continue; + } + + let type = objs1[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 1, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + + } else if (type == 11) { + + op.frontData.push({ + "type": 2, + "Text": item.get("text"), + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": "#000000", + "FontBackgroudColor": item.get("textBackgroundColor"), + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y": parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + "circleCenter": objs1[idx - 1].circleCenter + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 3, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "Exceed": objs1[idx - 1].exceed, + "AutoWrap": objs1[idx - 1].autoWrap, + "FixSize": objs1[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs1[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 4, + "name": objs1[idx - 1].name, + "FontColor": objs1[idx - 1].color, + "DefaultText": objs1[idx - 1].val, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 5, + "name": objs1[idx - 1].name, + "FontSize": parseInt(objs1[idx - 1].fontSize), + "DefaultText": objs1[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontFamily": objs1[idx - 1].font, + "FontColor": objs1[idx - 1].color, + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.frontData.push({ + "type": 6, + "name": objs1[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": objs1[idx - 1].start, + "add": parseInt(objs1[idx - 1].add), + "x": parseInt((xy[0] - background_image1.left).toFixed(0)), + "y": parseInt((xy[1] - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + //obj2 + for (let item of _objs2) { + // 跳过辅助线 + if (item.isGuideLine) { + continue; + } + + let idx = getIndex(item, canvas2); + if (idx == 0) continue;//background-image + + // 安全检查:确保 objs2[idx - 1] 存在且有 type 属性 + if (!objs2[idx - 1] || typeof objs2[idx - 1].type === 'undefined') { + continue; + } + + let type = objs2[idx - 1].type; + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 1, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].black ? "#000000" : "@", + "transparency": item.get("opacity"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 3) { + + } else if (type == 11) { + + op.frontData.push({ + "type": 2, + "Text": item.get("text"), + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": "#000000", + "FontBackgroudColor": item.get("textBackgroundColor"), + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y": parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + "circleCenter": objs2[idx - 1].circleCenter + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 3, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "DefaultText": item.get("text"), + "FontSize": parseInt(item.get("fontSize")), + "FontBackgroudColor": item.get("textBackgroundColor"), + "FontColor": item.get("fill"), + "Exceed": objs2[idx - 1].exceed, + "AutoWrap": objs2[idx - 1].autoWrap, + "FixSize": objs2[idx - 1].fixSize, + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + align: objs2[idx - 1].align + }); + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 4, + "name": objs2[idx - 1].name, + "FontColor": objs2[idx - 1].color, + "DefaultText": objs2[idx - 1].val, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 9) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 5, + "name": objs2[idx - 1].name, + "DefaultText": objs2[idx - 1].val, + "HideText": objs1[idx - 1].show, + "FontSize": parseInt(objs2[idx - 1].fontSize), + "FontFamily": objs2[idx - 1].font, + "FontColor": objs2[idx - 1].color, + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } else if (type == 10) { + let xy = getAbsoluteXY(item); + op.backData.push({ + "type": 6, + "name": objs2[idx - 1].name, + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": item.get("fill"), + "start": parseInt(objs2[idx - 1].start), + "add": parseInt(objs2[idx - 1].add), + "x": parseInt((xy[0] - background_image2.left).toFixed(0)), + "y": parseInt((xy[1] - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)) + }); + } + } + _save(op, callback); + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) +} + +// 重新生成二维码和条形码(加载文件后调用) +// 将 regenerateQrCodesAndBarcodes 附加到 window 对象,确保全局可访问 +window.regenerateQrCodesAndBarcodes = function regenerateQrCodesAndBarcodes(canvas, objsArray) { + if (!canvas || !objsArray || !Array.isArray(objsArray)) return; + + const objects = canvas.getObjects(); + + // 遍历对象(跳过背景图片,索引 0) + for (let i = 1; i < objects.length; i++) { + const obj = objects[i]; + if (!obj || obj.type !== 'image') continue; + + const objIndex = i - 1; + if (objIndex >= objsArray.length) continue; + + const objMeta = objsArray[objIndex]; + if (!objMeta || (objMeta.type !== 8 && objMeta.type !== 9)) continue; + + // type == 8: 二维码 - 复用 ui.js 中的生成逻辑 + if (objMeta.type === 8) { + const qr = jrQrcode.getQrBase64(objMeta.val || 'https://www.cardsoon.com', { + padding: 0, + foreground: objMeta.color || '#000000' + }); + obj.setSrc(qr, function (img) { + canvas.renderAll(); + }); + } + // type == 9: 条形码 - 复用 ui.js 中的生成逻辑 + else if (objMeta.type === 9) { + JsBarcode("#barcode", objMeta.val || '123456789', { + margin: 0, + lineColor: objMeta.color || '#000000', + fontSize: objMeta.fontSize || 18, + font: objMeta.font || 'Arial', + displayValue: !objMeta.show + }); + const barcode = document.getElementById('barcode'); + const bar = barcode.toDataURL("image/png"); + obj.setSrc(bar, function (img) { + canvas.renderAll(); + }); + } + } +} + +// open() 函数在 output.js 中定义,因为 output.js 先于 core.js 加载 +// 使用 window.openFile 避免与浏览器原生的 window.open 冲突 +window.openFile = function open(file, jAlready) { + if (!file) return; + if (typeof window.soonBindFileMeta === 'function') window.soonBindFileMeta(file); + function doOpenWithJson(j) { + if (!j || !background_image) return; + if (!j.f || !j.f.objects || !Array.isArray(j.f.objects) || j.f.objects.length === 0) return; + if (!j.b || !j.b.objects || !Array.isArray(j.b.objects) || j.b.objects.length === 0) { + j.b = { version: '4.6.0', objects: [], hoverCursor: 'move' }; + } + openAs.name = file; + var left = background_image.left; + var top = background_image.top; + var new_left = j.f.objects[0].left; + var new_top = j.f.objects[0].top; + for (var i = 0; i < j.f.objects.length; i++) { + j.f.objects[i].left = j.f.objects[i].left - new_left + left; + j.f.objects[i].top = j.f.objects[i].top - new_top + top; + } + if (j.f.objects[0] && !j.f.objects[0].src) j.f.objects[0].src = soonAsset('bg_front_1.png'); + if (j.b.objects && j.b.objects[0] && !j.b.objects[0].src) j.b.objects[0].src = soonAsset('bg_back.png'); + var fo = j.fo || [], bo = j.bo || []; + var placeholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + fo.forEach(function(objMeta, idx) { + if ((objMeta.type === 8 || objMeta.type === 9) && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) j.f.objects[idx + 1].src = placeholder; + else if (objMeta.type === 2 && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) j.f.objects[idx + 1].src = soonAsset('outpic_extra.png'); + }); + bo.forEach(function(objMeta, idx) { + if ((objMeta.type === 8 || objMeta.type === 9) && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) j.b.objects[idx + 1].src = placeholder; + else if (objMeta.type === 2 && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) j.b.objects[idx + 1].src = soonAsset('outpic_extra.png'); + }); + canvas1.loadFromJSON(j.f, function() { + if (typeof window.unlockAllObjects === 'function') window.unlockAllObjects(canvas1); + if (typeof window.regenerateQrCodesAndBarcodes === 'function') window.regenerateQrCodesAndBarcodes(canvas1, fo); + if (typeof window.updateList === 'function') window.updateList(); + canvas1.renderAll(); + }); + canvas2.loadFromJSON(j.b, function() { + if (typeof window.unlockAllObjects === 'function') window.unlockAllObjects(canvas2); + if (typeof window.regenerateQrCodesAndBarcodes === 'function') window.regenerateQrCodesAndBarcodes(canvas2, bo); + if (typeof window.updateList === 'function') window.updateList(); + canvas2.renderAll(); + var objects1 = canvas1.getObjects(), objects2 = canvas2.getObjects(); + if (objects1 && objects1.length > 0) { + background_image1 = objects1[0]; + background_image2 = objects2 && objects2.length > 0 ? objects2[0] : null; + objs2 = j.bo || []; + objs1 = j.fo || []; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + background_image = background_image1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + background_image = background_image2 || background_image1; + step = step2; + } + canvas1.renderAll(); + canvas2.renderAll(); + if (typeof window.updateList === 'function') window.updateList(); + else setTimeout(function() { if (typeof window.updateList === 'function') window.updateList(); }, 100); + setTimeout(function() { + recordObjs1.push(JSON.stringify(objs1)); + var props = window.TO_JSON_PROPERTIES || ["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", "evented"]; + var j1 = canvas1.toJSON(props); + if (j1.objects && j1.objects[0]) j1.objects[0].hoverCursor = "default"; + recordJson1.push(j1); + recordObjs2.push(JSON.stringify(objs2)); + var j2 = canvas2.toJSON(props); + recordJson2.push(j2); + }, 0); + } + }); + } + if (arguments.length >= 2 && jAlready) { + doOpenWithJson(jAlready); + openAs.name = file; + return; + } + if (window.platformBridge && window.platformBridge.readJsonFile) { + return window.platformBridge.readJsonFile(file).then(function(j) { + if (j) { + doOpenWithJson(j); + openAs.name = file; + } + }).catch(function() {}); + } + try { + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (!fs || file.indexOf('soondesign_session:') === 0) return; + if (!fs.existsSync(file)) return; + var fsData = fs.readFileSync(file); + var j; + try { j = JSON.parse(fsData.toString()); } catch (e) { return; } + doOpenWithJson(j); + } catch (e) {} +}; + +// 清理不需要的 src 字段(保留自定义图片的 src) +// 将 cleanupSrcFields 附加到 window 对象,确保在 ui.js 中的 saveAs 函数也能访问 +window.cleanupSrcFields = function cleanupSrcFields(jsonObjects, objsArray) { + if (!jsonObjects || !Array.isArray(jsonObjects)) return; + if (!objsArray || !Array.isArray(objsArray)) return; + + for (let i = 0; i < jsonObjects.length; i++) { + let obj = jsonObjects[i]; + // 只处理 fabric.Image 对象且有 src 字段的 + if (obj && obj.type === 'image' && obj.src) { + // 背景图片(索引 0)- 删除 src + if (i === 0) { + delete obj.src; + } + // 其他对象,通过 objs 数组判断类型 + else if (i - 1 < objsArray.length) { + let objMeta = objsArray[i - 1]; + // type == 2: 合成图片 - 删除 src + // type == 8: 二维码 - 删除 src + // type == 9: 条形码 - 删除 src + // type == 1: 自定义图片 - 必须保留 src + if (objMeta && (objMeta.type === 2 || objMeta.type === 8 || objMeta.type === 9)) { + delete obj.src; + } + // type == 1 不处理,保留 src + } + } + } +}; +// 向后兼容 +function cleanupSrcFields(jsonObjects, objsArray) { + return window.cleanupSrcFields(jsonObjects, objsArray); +} + +function applyCloudWriteResult(result, fallbackName) { + if (result && result.fileKey) { + openAs.name = result.fileKey; + return result.fileKey; + } + return fallbackName; +} + +function cloudSaveSuccessLabel(res, fallback) { + if (res && res.name) return res.name; + if (typeof window.soonDisplayFileName === 'function') { + return window.soonDisplayFileName(openAs.name || fallback); + } + return fallback || ''; +} + +function onCloudWriteDone(res, fallback, callback) { + applyCloudWriteResult(res, fallback); + layer.msg(language_str("saveSucc") + cloudSaveSuccessLabel(res, fallback)); + if (callback && typeof callback === 'function') callback(); +} + +function guardWebSave() { + return typeof window.soonGuardCloudSave === 'function' ? window.soonGuardCloudSave() : true; +} + +function saveAs(op1, callback) { + // 临时移除辅助线,保存后再恢复 + let guideLines1 = []; + let guideLines2 = []; + canvas1.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines1.push(obj); + canvas1.remove(obj); + } + }); + canvas2.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines2.push(obj); + canvas2.remove(obj); + } + }); + + const props = window.TO_JSON_PROPERTIES || ["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", "lockMovementX", "lockMovementY", "lockRotation", "lockScalingX", "lockScalingY", "lockSkewingX", "lockSkewingY", "evented"]; + let j = canvas1.toJSON(props); + let b = canvas2.toJSON(props); + + // 恢复辅助线 + guideLines1.forEach(obj => canvas1.add(obj)); + guideLines2.forEach(obj => canvas2.add(obj)); + canvas1.renderAll(); + canvas2.renderAll(); + + // 清理不需要的 src 字段 + cleanupSrcFields(j.objects, objs1); + cleanupSrcFields(b.objects, objs2); + + let o = { f: j, b: {}, fo: objs1, bo: objs2 }; + let con_o = Object.assign(o, op1); + if (!guardWebSave()) return; + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showSaveDialog({ + title: language_str("saveFile"), + filters: [{ name: 'Soon File Type', extensions: ['soon'] }], + defaultPath: openAs.name || undefined + }).then(function(result) { + if (result.canceled) return; + var fp = result.filePath || (result.fileHandle && result.fileHandle.name); + if (!fp) return; + var pathExt = (typeof window !== 'undefined' && window.path && window.path.extname) ? window.path.extname(fp) : (fp.indexOf('.') >= 0 ? fp.slice(fp.lastIndexOf('.')) : ''); + if (pathExt !== '.soon') fp = (fp || 'design').replace(/\.soon$/i, '') + '.soon'; + var content = JSON.stringify(con_o); + con_o.soonType = 1; + if (window.platformBridge && window.platformBridge.writeFile) { + window.platformBridge.writeFile(fp, content).then(function (res) { + onCloudWriteDone(res, fp, callback); + }).catch(function () {}); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFileSync(result.filePath || fp, content, 'utf8'); + openAs.name = result.filePath || fp; + layer.msg(language_str("saveSucc") + openAs.name); + saveHistory(); + if (callback && typeof callback === 'function') callback(); + } + }).catch(function(err) {}); +} + +function save(op1, callback) { + // 临时移除辅助线,保存后再恢复 + let guideLines1 = []; + let guideLines2 = []; + canvas1.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines1.push(obj); + canvas1.remove(obj); + } + }); + canvas2.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines2.push(obj); + canvas2.remove(obj); + } + }); + + const props = window.TO_JSON_PROPERTIES || ["selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline", "lockMovementX", "lockMovementY", "lockRotation", "lockScalingX", "lockScalingY", "lockSkewingX", "lockSkewingY", "evented"]; + let j = canvas1.toJSON(props); + let b = canvas2.toJSON(props); + + // 恢复辅助线 + guideLines1.forEach(obj => canvas1.add(obj)); + guideLines2.forEach(obj => canvas2.add(obj)); + canvas1.renderAll(); + canvas2.renderAll(); + + // 清理不需要的 src 字段 + cleanupSrcFields(j.objects, objs1); + cleanupSrcFields(b.objects, objs2); + + let o = { f: j, b: {}, fo: objs1, bo: objs2 }; + let con_o = Object.assign(o, op1); + if (!guardWebSave()) return; + if (openAs.name != "") { + con_o.soonType = 1; + var content = JSON.stringify(con_o); + if (window.platformBridge && window.platformBridge.writeFile) { + window.platformBridge.writeFile(openAs.name, content).then(function (res) { + onCloudWriteDone(res, openAs.name, callback); + }).catch(function () {}); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFileSync(openAs.name, content, 'utf8'); + layer.msg(language_str("saveSucc") + openAs.name); + saveHistory(); + if (callback && typeof callback === 'function') callback(); + } + return; + } + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showSaveDialog({ + title: language_str("saveFile"), + filters: [{ name: 'Soon File Type', extensions: ['soon'] }], + defaultPath: openAs.name || undefined + }).then(async function(result) { + if (result.canceled) return; + var fp = result.filePath || (result.fileHandle && result.fileHandle.name); + if (!fp) return; + var pathExt = (typeof window !== 'undefined' && window.path && window.path.extname) ? window.path.extname(fp) : (fp.indexOf('.') >= 0 ? fp.slice(fp.lastIndexOf('.')) : ''); + if (pathExt !== '.soon') fp = (fp || 'design').replace(/\.soon$/i, '') + '.soon'; + var content = JSON.stringify(con_o); + con_o.soonType = 1; + if (window.platformBridge && window.platformBridge.writeFile) { + window.platformBridge.writeFile(fp, content).then(function (res) { + onCloudWriteDone(res, fp, callback); + }).catch(function () {}); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFileSync(result.filePath || fp, content, 'utf8'); + openAs.name = result.filePath || fp; + layer.msg(language_str("saveSucc") + openAs.name); + saveHistory(); + if (callback && typeof callback === 'function') callback(); + } + }).catch(function(err) {}); +} + +window.saveHistory = function saveHistory() { + if (typeof window.soonIsWebPortal === 'function' && window.soonIsWebPortal()) return; + function doWrite(j) { + var currentPath = openAs.name; + if (!currentPath) return; + var pathForHistory = currentPath; + if (window.platformBridge && currentPath.indexOf('soondesign_session:') !== 0 && currentPath.indexOf('soondesign_file:') !== 0) { + pathForHistory = 'soondesign_session:' + (currentPath || 'design.soon'); + } + var newList = (j.history || []).filter(function(item) { return item.path !== pathForHistory && item.path !== currentPath; }); + newList.unshift({ time: getDate(), path: pathForHistory, type: 1 }); + if (newList.length > 20) newList = newList.slice(0, 20); + j.history = newList; + if (window.platformBridge && window.platformBridge.writeHistory) { + window.platformBridge.writeHistory(j); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs && fullPath) { + try { fs.writeFileSync(fullPath, JSON.stringify(j), 'utf8'); } catch (e) {} + } + } + if (window.platformBridge && window.platformBridge.readHistory) { + window.platformBridge.readHistory().then(function(j) { + if (!j || !Array.isArray(j.history)) j = { history: [] }; + doWrite(j); + }).catch(function() { doWrite({ history: [] }); }); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + var j = { history: [] }; + if (fs && fullPath) { + try { + if (fs.existsSync(fullPath)) { + var content = fs.readFileSync(fullPath).toString(); + if (content.trim()) j = JSON.parse(content); + } + } catch (e) { j = { history: [] }; } + } + if (!Array.isArray(j.history)) j.history = []; + doWrite(j); +}; +// 向后兼容 +function saveHistory() { + return window.saveHistory(); +} + +// open() 函数已移至 core.js,因为它在 core.js 的 addBackground() 中被调用 + +function clearAll() { + //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; + //let j = JSON.parse(blank); + //canvas1.setZoom(1); + //canvas2.setZoom(1); + //canvas1.loadFromJSON(j.f); + //canvas2.loadFromJSON(j.b); + let t_objs1 = canvas1.getObjects(); + let t_objs2 = canvas2.getObjects(); + for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]); + } + for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]); + } + objs2 = []; + objs1 = []; + step1.val = 0; + step2.val = 0; + recordJson1 = []; + recordJson2 = []; + recordObjs1 = []; + recordObjs2 = []; + if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + } else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + } + openAs.name = ""; + canvas1.renderAll(); + canvas2.renderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + } +} + +// 将 clearAll 附加到 window 对象,确保全局可访问 +window.clearAll = clearAll; + diff --git a/frontend-web/js/design1/ui.js b/frontend-web/js/design1/ui.js new file mode 100644 index 0000000..0c82cb6 --- /dev/null +++ b/frontend-web/js/design1/ui.js @@ -0,0 +1,2436 @@ +// 移至上层和移至下层的事件处理在下方(第 2179 行),避免重复绑定 + +var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false; +$('input').focus(function () { + _inputEdit = true; +}); +$('input').blur(function () { + _inputEdit = false; +}); +$('textarea').focus(function () { + _inputEdit = true; +}); +$('textarea').blur(function () { + _inputEdit = false; +}); +$("body").keyup(function (event) { + // 兼容三端�?modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta�? + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释�? + _keyctrl = false; + }; + if (event.keyCode == 67) { + _keyc = false; + }; +}) +$("body").keydown(function (event) { + // 统一的快捷键处理(兼�?Windows/Linux: Ctrl, Mac: Cmd/Meta�? + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的�? + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $("#new").click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $("#open").click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $("#save").click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + if (typeof window.output === 'function') { + window.output(null, saveAs); + } + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $("#previous").click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $("#next").click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + window.pasteFromClipboard(); + return; + } + + // CTRL/CMD + SHIFT + DEL (Windows) �?CMD + SHIFT + Backspace (Mac) - 清空 + if (isModifierKey && event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#clean").click(); + return; + } + + // CTRL/CMD + DEL (Windows) �?CMD + Backspace (Mac) - 删除 + if (isModifierKey && !event.shiftKey && (event.keyCode == 46 || event.keyCode == 8)) { + event.preventDefault(); + $("#delete").click(); + return; + } + + } + // 兼容三端�?modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta�? + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd�? + _keyctrl = true; + if (_keyc && _textEdit) { + //同时按下且在编辑状�? + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + } + } + if (event.keyCode == 67) { + _keyc = true; + if (_keyctrl & _textEdit) { + //同时按下且在编辑状�? + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + } + } + if (event.keyCode == 37) { + //left + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let x = currentCanvas.getActiveObject().get('left'); + x += v; + currentCanvas.getActiveObject().set('left', x); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + if (event.keyCode == 38) { + //up + let v = -1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let y = currentCanvas.getActiveObject().get('top'); + y += v; + currentCanvas.getActiveObject().set('top', y); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + if (event.keyCode == 39) { + //right + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let x = currentCanvas.getActiveObject().get('left'); + x += v; + currentCanvas.getActiveObject().set('left', x); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + if (event.keyCode == 40) { + //down + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let y = currentCanvas.getActiveObject().get('top'); + y += v; + currentCanvas.getActiveObject().set('top', y); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + // 普通删除键(DEL键,没有按CTRL/CMD�? + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + // 使用 window.canvas 作为后备,确保在 eval() 加载 .jsc 时也能访问 + const currentCanvas = window.canvas || canvas; + const currentObjs = window.objs || objs; + if (!currentCanvas) { + return; + } + let actObjs = currentCanvas.getActiveObjects(); + let allObjs = currentCanvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item, currentCanvas);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + currentObjs.splice(i - 1, 1); + currentCanvas.remove(item); + flag = 1; + + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + currentCanvas.discardActiveObject(); + if (flag == 1) { + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + currentCanvas.renderAll(); + if (typeof window.handleObjectSelected === 'function') { + window.handleObjectSelected(); + } + } + } +}); +// pre_add_image 已在 design1.js 中定义为全局变量 +$("#addPic").click(function () { + OpenDialog(); + function OpenDialog() { + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showOpenDialog({ + title: language_str("selectPic"), + buttonLabel: language_str("comf"), + filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] + }).then(function(result) { + if (result.canceled) return; + var url = (result.files && result.files[0]) ? URL.createObjectURL(result.files[0]) : (result.filePaths && result.filePaths[0]); + if (!url) return; + fabric.Image.fromURL(url, function (image) { + //image.left = pointer.x; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image; + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 1; + }); + //canvas.add(image); + }); + }).catch(err => { + }) + } +}); +$("#addOutPic").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 2; +}); +$("#addText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 3; +}); + +$("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; +}); +$("#addOutText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 4; +}); +$("#addRect").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 5; +}); +$("#addCircle").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 6; +}); +$("#addLine").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 7; +}); +$("#addQrCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 8; +}); +$("#addBarCode").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 9; +}); +$("#addCounter").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addCircleText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + addState = 10; +}); +$("#version_change").click(function () { + if (bg_version == 1) { + bg_version = 2; + } else { + bg_version = 1; + } + // 类型改变 + $("#source_front").attr("src", soonAsset("front_bg") + bg_version + "_1.png"); + canvas.renderAll(); + +}); +$("#front_side").click(function () { + $(this).addClass("ui-button-active"); + $("#back_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").show(); + $(".canvas-container:eq(1)").hide(); + canvas = canvas1; + objs = objs1; + background_image = background_image1; + window.canvas = canvas; + window.objs = objs; + window.background_image = background_image; + next_objs = next_objs1; + pre_objs = pre_objs1; + next_json = next_json1; + pre_json = pre_json1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; + if (typeof window.updateList === 'function') { + window.updateList(); + } + + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//背景 +}); +$("#back_side").click(function () { + $(this).addClass("ui-button-active"); + $("#front_side").removeClass("ui-button-active"); + $(".canvas-container:eq(0)").hide(); + $(".canvas-container:eq(1)").show(); + canvas = canvas2; + objs = objs2; + background_image = background_image2; + window.canvas = canvas; + window.objs = objs; + window.background_image = background_image; + next_objs = next_objs2; + pre_objs = pre_objs2; + next_json = next_json2; + pre_json = pre_json2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; + if (typeof window.updateList === 'function') { + window.updateList(); + } + $("#line_control").hide(); + $("#base_control").hide(); + $("#pic_control").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#circle_text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + $("#component_type").text(language_str("bg"));//"背景" +}); +$("#top_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinY = getCoordsMinY(coords); + let bgMinY = getCoordsMinY(background_image.getCoords(true)); + let deltaY = bgMinY - objMinY; + obj.set("top", obj.get("top") + deltaY); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最小Y值 + let minTop = Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + if (itemMinY < minTop) { + minTop = itemMinY; + } + } + // 将所有对象对齐到最小Y值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + let deltaY = minTop - itemMinY; + item.set("top", item.get("top") + deltaY); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + +}); +$("#bottom_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMaxY = getCoordsMaxY(coords); + let bgMaxY = getCoordsMaxY(background_image.getCoords(true)); + let deltaY = bgMaxY - objMaxY; + obj.set("top", obj.get("top") + deltaY); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最大Y值 + let maxBottom = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxY = getCoordsMaxY(coords); + if (itemMaxY > maxBottom) { + maxBottom = itemMaxY; + } + } + // 将所有对象对齐到最大Y值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxY = getCoordsMaxY(coords); + let deltaY = maxBottom - itemMaxY; + item.set("top", item.get("top") + deltaY); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + +}); +$("#center_y_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinY = getCoordsMinY(coords); + let objMaxY = getCoordsMaxY(coords); + let objCenterY = (objMinY + objMaxY) / 2; + let bgCoords = background_image.getCoords(true); + let bgMinY = getCoordsMinY(bgCoords); + let bgMaxY = getCoordsMaxY(bgCoords); + let bgCenterY = (bgMinY + bgMaxY) / 2; + let deltaY = bgCenterY - objCenterY; + obj.set("top", obj.get("top") + deltaY); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的中心Y值 + let minTop = Infinity; + let maxBottom = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + let itemMaxY = getCoordsMaxY(coords); + if (itemMinY < minTop) minTop = itemMinY; + if (itemMaxY > maxBottom) maxBottom = itemMaxY; + } + let groupCenterY = (minTop + maxBottom) / 2; + // 将所有对象对齐到中心Y值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinY = getCoordsMinY(coords); + let itemMaxY = getCoordsMaxY(coords); + let itemCenterY = (itemMinY + itemMaxY) / 2; + let deltaY = groupCenterY - itemCenterY; + item.set("top", item.get("top") + deltaY); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#left_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinX = getCoordsMinX(coords); + let bgMinX = getCoordsMinX(background_image.getCoords(true)); + let deltaX = bgMinX - objMinX; + obj.set("left", obj.get("left") + deltaX); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最小X值 + let minLeft = Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + if (itemMinX < minLeft) { + minLeft = itemMinX; + } + } + // 将所有对象对齐到最小X值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + let deltaX = minLeft - itemMinX; + item.set("left", item.get("left") + deltaX); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#right_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMaxX = getCoordsMaxX(coords); + let bgMaxX = getCoordsMaxX(background_image.getCoords(true)); + let deltaX = bgMaxX - objMaxX; + obj.set("left", obj.get("left") + deltaX); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的最大X值 + let maxRight = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxX = getCoordsMaxX(coords); + if (itemMaxX > maxRight) { + maxRight = itemMaxX; + } + } + // 将所有对象对齐到最大X值 + let groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMaxX = getCoordsMaxX(coords); + let deltaX = maxRight - itemMaxX; + item.set("left", item.get("left") + deltaX); + item.setCoords(); + groupItems.push(item); + } + //BUG1 + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#center_x_alignment").click(function () { + let _objs = canvas.getActiveObjects(); + if (_objs.length == 1) { + //只选中了一�? + let idx = getIndex(_objs[0]); + let type = objs[idx - 1].type; + let other = 0;//strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get("strokeWidth") + } + let obj = _objs[0]; + obj.setCoords(); + let coords = obj.getCoords(true); + let objMinX = getCoordsMinX(coords); + let objMaxX = getCoordsMaxX(coords); + let objCenterX = (objMinX + objMaxX) / 2; + let bgCoords = background_image.getCoords(true); + let bgMinX = getCoordsMinX(bgCoords); + let bgMaxX = getCoordsMaxX(bgCoords); + let bgCenterX = (bgMinX + bgMaxX) / 2; + let deltaX = bgCenterX - objCenterX; + obj.set("left", obj.get("left") + deltaX); + obj.setCoords(); + canvas.renderAll(); + window.recordState(); + return; + } + // 多个对象对齐:找到所有对象边界框的中心X值 + let minLeft = Infinity; + let maxRight = -Infinity; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + let itemMaxX = getCoordsMaxX(coords); + if (itemMinX < minLeft) minLeft = itemMinX; + if (itemMaxX > maxRight) maxRight = itemMaxX; + } + let groupCenterX = (minLeft + maxRight) / 2; + // 将所有对象对齐到中心X值 + groupItems = []; + for (let item of _objs) { + item.setCoords(); + let coords = item.getCoords(true); + let itemMinX = getCoordsMinX(coords); + let itemMaxX = getCoordsMaxX(coords); + let itemCenterX = (itemMinX + itemMaxX) / 2; + let deltaX = groupCenterX - itemCenterX; + item.set("left", item.get("left") + deltaX); + item.setCoords(); + groupItems.push(item); + } + canvas.discardActiveObject(); + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas, + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + canvas.renderAll(); + window.recordState(); +}); +$("#up_obj").click(function () {//上移,放到前�?数组中在后面 + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + //console.log(objs); + + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } +}); +$("#down_obj").click(function () {//下移,放到后�?数组中在前面 + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } +}); +$("#set_bg").click(function () {//设置为背�? + let obj = canvas.getActiveObject(); + let idx = getIndex(obj); + const scaleX = 2787 / obj.width + const scaleY = 2787 / obj.height + // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 + const scaledWidth = obj.width * scaleX; + const scaledHeight = obj.height * scaleY; + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - scaledWidth) / 2, + top: ($('#canvas1').height() - scaledHeight) / 2, + }); + $('#text_w').val(2787) + $('#text_h').val(2787) + $('#text_x').val(0) + $('#text_y').val(0) + if (obj.get("selectable")) { + obj.set("selectable", false); + $("#lock_img").attr("src", soonAsset("lock20.png")) + } + canvas.sendToBack(obj); + canvas.bringForward(obj); + canvas.renderAll(); + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(0); + } +}); + +// ========================================== +// onStateRestored 函数在 core.js 中定义并附加到 window 对象 +// ========================================== +$("#previous").click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + + if (step.val == 0) return; + + step.val--; + + // 判断当前是正面还是背面 + if ($("#front_side").hasClass("ui-button-active")) { + // 1. 恢复业务数据 + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + // 2. 恢复画布 (使用回调函数) + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson1[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); // 图片加载完才执行这一行 + } + window._isApplyingHistory = false; + }); + } else { + // 背面逻辑 + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } +}); +// ========================================== +$("#next").click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + + // 获取当前历史记录的最大长度 + let maxLen = $("#front_side").hasClass("ui-button-active") ? recordObjs1.length : recordObjs2.length; + + if (step.val >= maxLen - 1) return; + + step.val++; + + if ($("#front_side").hasClass("ui-button-active")) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson1[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); // 图片加载完才执行这一行 + } + window._isApplyingHistory = false; + }); + } else { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } +}); + +$("#clean").click(function () { + objs.splice(0, objs.length); + + let allObjs = canvas.getObjects(); + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item); + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}); +$("#rotate").click(function () { + let obj = canvas.getActiveObject(); + obj.rotate(Math.round((obj.get("angle") + 90) % 360)); + $("#text_r").val(obj.get("angle")); + canvas.renderAll(); + updateControls(); + window.recordState(); +}); +$('#vertical_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipY", !obj.get("flipY")); + canvas.renderAll(); + updateControls(); + window.recordState(); +}); +$('#align_obj').click(function () { + let obj = canvas.getActiveObject(); + obj.set("flipX", !obj.get("flipX")); + canvas.renderAll(); + updateControls(); + window.recordState(); +}); +$("#text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�?恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�?恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + window.recordState(); + canvas.renderAll(); +}) +$('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + window.recordState(); + canvas.renderAll(); +}) +$('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + window.recordState(); + canvas.renderAll(); +}) + +$("#out_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�?恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已�? + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未�? + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#lock").off('click').on('click', function (e) { + e.stopPropagation(); + if (!canvas.getActiveObject()) return; + let obj = canvas.getActiveObject(); + // 检查是否已锁定(通过lockMovementX判断,因为锁定时会设置所有lock属性) + let isLocked = obj.get("lockMovementX") === true; + let shouldLock = !isLocked; + + // 锁定/解锁所有编辑操作,但保持选中状态(不取消选中) + obj.set({ + lockMovementX: shouldLock, + lockMovementY: shouldLock, + lockRotation: shouldLock, + lockScalingX: shouldLock, + lockScalingY: shouldLock, + lockSkewingX: shouldLock, + lockSkewingY: shouldLock, + // 锁定时不可选中,解锁时可选中(但保持当前选中状态) + selectable: !shouldLock, + evented: !shouldLock + }); + if (shouldLock) { + $("#lock_img").attr("src", soonAsset("lock20.png"))//已锁定 + } else { + $("#lock_img").attr("src", soonAsset("unlock20.png"))//已解锁 + } + updateControls(); + window.recordState(); + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas.renderAll(); +}); +$("#text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#circle_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#out_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#rect_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#rect_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#circle_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#circle_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); + +$("#copy").click(function () { + window.copySelection(); +}) +// 粘贴 +$("#paste").click(function () { + window.pasteFromClipboard(); +}) + +$("#save").click(function () { + if (typeof window.output === 'function') { + window.output(); + } +}); +function getDate() { + let date = new Date(); + return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); +} +function getAbsoluteXY(_obj) { + let coord = _obj.get("lineCoords"); + return [_obj.get("left"), _obj.get("top")] +} +$("#open").click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件�? + if (typeof window.output === 'function') { + window.output(OpenDialog); + } + layer.close(index); + + }, function (index) { + OpenDialog(); + }); + function OpenDialog() { + if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return; + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showOpenDialog({ + title: "请选择文件", + buttonLabel: language_str("comf"), + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }).then(function(result) { + if (result.canceled) return; + var file = (result.files && result.files[0]) || result.file; + function openWithKey(fileKey, j) { + var type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + if (type == 2) { + if (window.platformBridge) window.platformBridge.openDesignPage(fileKey, type); + return; + } + if (typeof window.openFile === 'function') { + window.openFile(fileKey, j); + } + } + function importAndOpen(j, fileName) { + if (!window.platformBridge || !window.platformBridge.importSoonFile) return; + window.platformBridge.importSoonFile(fileName || 'design.soon', j).then(function(res) { + if (res && res.fileKey) openWithKey(res.fileKey, j); + }).catch(function () {}); + } + if (file && file.text) { + file.text().then(function(t) { + var j = JSON.parse(t); + importAndOpen(j, file.name || 'design.soon'); + }).catch(function() {}); + return; + } + var filePath = result.filePaths && result.filePaths[0]; + if (!filePath) return; + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + var fsData = fs.readFileSync(filePath); + importAndOpen(JSON.parse(fsData.toString()), filePath); + } + }).catch(function(err) {}); + } +}); +//let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":2787,\"height\":2787,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; +let t_objs1 = canvas1.getObjects(); +let t_objs2 = canvas2.getObjects(); +for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]); +} +for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]); +} +objs2 = []; +objs1 = []; +step1.val = 0; +step2.val = 0; +recordJson1 = []; +recordJson2 = []; +recordObjs1 = []; +recordObjs2 = []; +if ($("#front_side").hasClass("ui-button-active")) { + objs = objs1; + recordObjs = recordObjs1; + recordJson = recordJson1; + step = step1; +} else if ($("#back_side").hasClass("ui-button-active")) { + objs = objs2; + recordObjs = recordObjs2; + recordJson = recordJson2; + step = step2; +} +openAs.name = ""; +canvas1.renderAll(); +canvas2.renderAll(); +if (typeof window.updateList === 'function') { + window.updateList(); +} +$("#new").click(function () { + if (step.val == 0) { + if (ipcRenderer) ipcRenderer.send('open-first-page'); + else if (window.platformBridge) window.platformBridge.openFirstPage(); + return; + } + layer.confirm('' + language_str("whetherSave") + '', function (index) {//confirm 是否保存当前文件�? + if (typeof window.output === 'function') { + window.output(function() { if (ipcRenderer) ipcRenderer.send('open-first-page'); else if (window.platformBridge) window.platformBridge.openFirstPage(); }); + } + }, function (index) { + if (ipcRenderer) ipcRenderer.send('open-first-page'); + else if (window.platformBridge) window.platformBridge.openFirstPage(); + }); +}); + +// =========================================================== +$("#text_y").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + if ($("#text_y").val() == "") { + $("#text_y").val(0) + } + let val = background_image.top; + if ($("#text_y").val() != "") { + val = background_image.top + parseInt($("#text_y").val()); + } + canvas.getActiveObject().set("top", val) + canvas.renderAll(); + window.recordState(); +}) +$("#text_x").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + if ($("#text_x").val() == "") { + $("#text_x").val(0) + } + let val = background_image.left; + if ($("#text_x").val() != "") { + val = background_image.left + parseInt($("#text_x").val()); + } + canvas.getActiveObject().set("left", val) + canvas.renderAll(); + window.recordState(); +}) +$("#text_w").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + let val = "0"; + if ($("#text_w").val() != "") { + val = $("#text_w").val(); + } + let idx = getIndex(canvas.getActiveObject()); + if (idx <= 0 || !objs[idx - 1]) return; + let type = objs[idx - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("width", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleX", parseInt(val) / canvas.getActiveObject().get("width")) + } + + canvas.renderAll(); + window.recordState(); +}) +$("#text_h").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + let idx = getIndex(canvas.getActiveObject()); + if (idx <= 0 || !objs[idx - 1]) return; + if (objs[idx - 1].type == 7) { + if ($("#text_h").val() == "") { + $("#text_h").val(canvas.getActiveObject().get("strokeWidth")); + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#text_h").val())); + canvas.renderAll(); + return; + } + let val = "0"; + if ($("#text_h").val() != "") { + val = $("#text_h").val(); + } + let type = objs[idx - 1].type; + if (type == 3 || type == 4) { + canvas.getActiveObject().set("height", parseInt(val)) + } else { + canvas.getActiveObject().set("scaleY", parseInt(val) / canvas.getActiveObject().get("height")) + } + + canvas.renderAll(); + window.recordState(); +}) +$("#text_r").bind('input propertychange', function () { + if (!canvas.getActiveObject()) return; + if ($("#text_r").val() == "") { + $("#text_r").val("0") + } + $("#text_r").val(parseInt($("#text_r").val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($("#text_r").val())) + canvas.renderAll(); + window.recordState(); +}) +$("#out_pic_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_pic_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) + +$("#text_black").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].black = true; + } else { + objs[idx - 1].black = false; + } + window.recordState(); +}); +$("#out_text_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#out_text_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#qrcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#qrcode_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#qrcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64($("#qrcode_val").val(), { padding: 0, foreground: $(this).val() }); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + item.setSrc(qr, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#qrcode_val").bind('input propertychange', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#barcode_color").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].color = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#barcode_field_name").bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()); + objs[idx - 1].name = $("#barcode_field_name").val(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#text_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#text_back_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + window.recordState(); +}) +$("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); + canvas.renderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}) +$("#out_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#out_text_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#out_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#out_text_back_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + window.recordState(); +}) +// 监听合成文本输入框 (占位符) +$("#out_text_text").on('input propertychange', function () { + let val = $(this).val(); + + // 【核心修改】如果值为空,强制给 2 个空格 + if (val === "") { + val = " "; + } + + let obj = canvas.getActiveObject(); + if (obj) { + obj.set("text", val); + obj.setCoords(); + canvas.requestRenderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); + } +}); +$("#rect_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#rect_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#rect_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#rect_stroke").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#rect_stroke_width").bind('input propertychange', function () { + if ($("#rect_stroke_width").val() == "") { + $("#rect_stroke_width").val("0") + } else { + $("#rect_stroke_width").val(parseInt($("#rect_stroke_width").val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#rect_stroke_width").val())); + canvas.renderAll(); + window.recordState(); +}) +$("#rect_radius").bind('input propertychange', function () { + if ($("#rect_radius").val() == "") { + $("#rect_radius").val("0") + } else { + $("#rect_radius").val(parseInt($("#rect_radius").val())) + } + canvas.getActiveObject().set("rx", parseInt($("#rect_radius").val())); + canvas.getActiveObject().set("ry", parseInt($("#rect_radius").val())); + canvas.renderAll(); + window.recordState(); +}) +$("#circle_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_color").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#circle_stroke").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#circle_stroke").val()); + + canvas.renderAll(); + window.recordState(); +}) +$("#circle_stroke_width").bind('input propertychange', function () { + if ($("#circle_stroke_width").val() == "") { + $("#circle_stroke_width").val("0") + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set("strokeWidth", parseInt($("#circle_stroke_width").val())); + canvas.renderAll(); + window.recordState(); +}) +$("#counter_start").bind('input propertychange', function () { + canvas.getActiveObject().set("text", $("#counter_start").val()); + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].start = $(this).val(); + canvas.renderAll(); + window.recordState(); +}) +$("#counter_add").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].add = $(this).val(); + canvas.renderAll(); + window.recordState(); +}) +$("#counter_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#counter_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + window.recordState(); +}) +$("#line_color").bind('input propertychange', function () { + canvas.getActiveObject().set("stroke", $("#line_color").val()); + canvas.renderAll(); + window.recordState(); +}) +$("#line_dist").bind('input propertychange', function () { + if ($("#line_dist").val() == "") { + $("#line_dist").val(0) + } + canvas.getActiveObject().set("strokeDashArray", [parseInt($("#line_dist").val()), parseInt($("#line_dist").val())]) + canvas.renderAll(); + window.recordState(); +}) +// 监听普通文本输入 (占位符) +$("#text_text").on('input propertychange', function () { + // 1. 获取输入框的值,不要用 trim(),否则无法输入空格 + let val = $(this).val(); + + // 2. 核心修改:只有当删光时,才给 4 个空格作为占位符 + if (val === "") { + val = " "; // 4个空格,比原来的2个更宽,更好点 + } + // 3. 更新对象 + canvas.getActiveObject().set("text", val); + canvas.requestRenderAll(); // 使用 requestRenderAll 性能更好 + if (typeof window.updateList === 'function') { + window.updateList(); + } + window.recordState(); +}); +$("#barcode_val").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].val = $(this).val(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}) +$("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val(); + let displayValue = true; + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + displayValue = false; + } else { + objs[idx - 1].show = false; + displayValue = true; + } + //val = 0; // Remove this hack + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val * dpi / 72, font: objs[idx - 1].font, displayValue: displayValue }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}); +$("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val * dpi / 72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); +}) +$('#count_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); +$('#text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); + +$('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); + +$('#language_select').on('change', function () {//语言选择 + langua_ge($(this).find('option:selected').val()); + s_lan = $(this).find('option:selected').val(); + localStorage.setItem("lang", s_lan); +}); +$('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + objs[idx - 1].font = $(this).find('option:selected').text(); + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + window.recordState(); + }); + //window.recordState(); +}); +$('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + window.recordState(); +}); +$(".obj_list").on("click", "div", function () { + let idx = objs.length - 1 - $(this).index(); + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } +}); +$("#exceed").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].exceed = true; + } else { + objs[idx - 1].exceed = false; + } + window.recordState(); +}); +$("#autoWrap").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].autoWrap = true; + } else { + objs[idx - 1].autoWrap = false; + } + window.recordState(); +}); +$("#fixSize").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + + objs[idx - 1].fixSize = true; + } else { + objs[idx - 1].fixSize = false; + } + window.recordState(); +}); +$("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + window.recordState(); +}); + +// =========================================================== +$("#text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +$('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + window.recordState(); + canvas.renderAll() +}) +$('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + window.recordState(); + canvas.renderAll() +}) +$('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + window.recordState(); + canvas.renderAll() +}) + +$("#out_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_underline").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("underline", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("underline", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_linethrough").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("linethrough", false) + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("linethrough", true) + $(this).addClass("ui-radio-acitve") + } + window.recordState(); + canvas.renderAll(); + window.recordState(); +}); + +// =========================================================== +$("#delete").click(function () { + let actObjs = canvas.getActiveObjects(); + let allObjs = canvas.getObjects(); + let flag = 0; + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1); + canvas.remove(item); + flag = 1; + + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas.discardActiveObject(); + if (flag == 1) { + window.recordState(); + canvas.renderAll(); + } +}); +// 移至上层和移至下层:先取消所有绑定(包括第一处的绑定),然后重新绑定(确保只绑定一次) +// 注意:第一处的事件绑定(第 842 行和第 861 行)会被这里的 off('click') 取消 +$("#up_obj").off('click').on('click', function () {//上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } +}); +$("#down_obj").off('click').on('click', function () {//下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } +}); +$("#set_bg").click(function () {//设置为背景 + let obj = canvas.getActiveObject(); + let idx = getIndex(obj); + const scaleX = 2787 / obj.width + const scaleY = 2787 / obj.height + // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 + const scaledWidth = obj.width * scaleX; + const scaledHeight = obj.height * scaleY; + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - scaledWidth) / 2, + top: ($('#canvas1').height() - scaledHeight) / 2, + }); + $('#text_w').val(2787) + $('#text_h').val(2787) + $('#text_x').val(0) + $('#text_y').val(0) + // 设为背景时,锁定所有编辑操作,并设置为不可选中 + obj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + $("#lock_img").attr("src", soonAsset("lock20.png")) + canvas.sendToBack(obj); + canvas.bringForward(obj); + canvas.renderAll(); + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + + // 确保背景图(索引0)的锁定属性被正确设置 + setTimeout(() => { + let bgObj = canvas.getObjects()[0]; + if (bgObj) { + bgObj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + canvas.renderAll(); + } + }, 100); + + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(0); + } +}); +// 重复的lock点击事件已移除,使用第一处的绑定 +$("#text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#circle_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#out_text_back_transparent").click(function () { + canvas.getActiveObject().set("textBackgroundColor", ""); + $("#out_text_back_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#rect_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#rect_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#rect_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_transparent").click(function () { + canvas.getActiveObject().set("fill", ""); + $("#circle_color").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); +$("#circle_stroke_transparent").click(function () { + canvas.getActiveObject().set("stroke", ""); + $("#circle_stroke").val("#ffffff"); + canvas.renderAll(); + window.recordState(); +}); + +// =========================================================== +$("#about").click(async function () { + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
    ' + + '
    ' + + '
    Soon Design
    ' + + '
    ' + language_str("vers") + ' ' + version + '
    ' + + '
    ' + language_str("copyright") + '
    ' + + '
    ' + }); +}) +$("#help").click(function() { + if (window.platformBridge && window.platformBridge.openHelp) window.platformBridge.openHelp(); + else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-help-file'); +}); + +// =========================================================== +$('#display').on('click', function () { + if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return; + // 类型改变 + fabric.Image.fromURL(soonAsset('front_bg') + bg_version + '_1.png', function (i1) { + i1.left = background_image.left; + i1.top = background_image.top; + let img1 = i1; + + fabric.Image.fromURL(soonAsset('back_bg_1.png'), function (i2) { + i2.left = background_image.left; + i2.top = background_image.top; + let img2 = i2; + fabric.Image.fromURL(soonAsset('op_1.png'), function (i3) { + i3.left = background_image.left; + i3.top = background_image.top; + let img3 = i3; + if (typeof window.display_func === 'function') { + window.display_func(img1, img2, img3); + } + }) + }) + }) +}); +$("#output").click(function () { + // 另存为:传递 saveAs 函数作为第二个参数 + if (typeof window.output === 'function') { + window.output(null, saveAs); // 调用output函数,使用saveAs进行另存为 + } +}); + +// 确保移至上层和移至下层的事件绑定正确(在文件末尾再次确认) +// 取消所有绑定,然后重新绑定第二处的实现(第 2171 行和第 2189 行) +$(document).ready(function () { + setTimeout(function () { + $("#up_obj").off('click').on('click', function () { + let idx = getIndex(canvas.getActiveObject()); + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx]; + objs[idx] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } + }); + $("#down_obj").off('click').on('click', function () { + let idx = getIndex(canvas.getActiveObject()); + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()); + canvas.renderAll(); + let temp = objs[idx - 1]; + objs[idx - 1] = objs[idx - 2]; + objs[idx - 2] = temp; + window.recordState(); + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } + }); + }, 100); +}); diff --git a/frontend-web/js/design2-back.js b/frontend-web/js/design2-back.js new file mode 100644 index 0000000..ab42b03 --- /dev/null +++ b/frontend-web/js/design2-back.js @@ -0,0 +1,6144 @@ + + function desEncrypt(str, key = 'df6a551ca43181fc485f3043bcdd2fbc') { + var APIFMS + try { + var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key) + var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }) + APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext) + } catch (err) { + console.log('des 加密 -------------------------') + console.log(err) + } + return APIFMS + } + + (function (fabric) { + fabric.CurvedText = fabric.util.createClass(fabric.Object, { + type: 'curved-text', + diameter: 250, + kerning: 0, + text: '', + flipped: false, + fill: '#000', + fontFamily: 'Times New Roman', + fontSize: 24, // in px + fontWeight: 'normal', + fontStyle: '', // "normal", "italic" or "oblique". + cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'), + strokeStyle: null, + strokeWidth: 0, + initialize: function (text, options) { + options || (options = {}); + if (typeof text === 'object' && text) { + for (let key in text) { + this[key] = text[key] + } + } else { + this.text = text; + } + this.callSuper('initialize', options); + this.set('lockUniScaling', true); + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + }, + _getFontDeclaration: function () { + return [ + (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), + (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + cropCanvas: function (canvas) { + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.height, + pix = { x: [], y: [] }, n, + imageData = ctx.getImageData(0, 0, w, h), + fn = function (a, b) { return a - b }; + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + if (imageData.data[((y * w + x) * 4) + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // 如果没有找到任何非透明像素,直接返回,不进行裁剪 + if (pix.x.length === 0 || pix.y.length === 0) { + return; + } + pix.x.sort(fn); + pix.y.sort(fn); + n = pix.x.length - 1; + w = pix.x[n] - pix.x[0]; + h = pix.y[n] - pix.y[0]; + // 确保宽度和高度是有效的正数 + if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) { + return; + } + var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h); + canvas.width = w; + canvas.height = h; + ctx.putImageData(cut, 0, 0); + }, + getCircularText: function () { + var text = this.text, + diameter = this.diameter, + flipped = this.flipped, + kerning = this.kerning, + fill = this.fill, + inwardFacing = true, + startAngle = 0, + canvas = fabric.util.createCanvasElement(), + ctx = canvas.getContext('2d'), + cw, // character-width + x, // iterator + clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise + if (flipped) { + // startAngle = 180; + inwardFacing = false; + } + startAngle *= Math.PI / 180; // convert to radians + var d = document.createElement('div'); + d.style.fontFamily = this.fontFamily; + d.style.fontSize = this.fontSize + 'px'; + d.style.fontWeight = this.fontWeight; + d.style.fontStyle = this.fontStyle; + d.textContent = text; + document.body.appendChild(d); + var textHeight = d.offsetHeight; + document.body.removeChild(d); + canvas.width = canvas.height = diameter; + ctx.font = this._getFontDeclaration(); + if (inwardFacing) { text = text.split('').reverse().join('') }; + ctx.translate(diameter / 2, diameter / 2); // Move to center + startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward + ctx.textBaseline = 'middle'; // Ensure we draw in exact center + ctx.textAlign = 'center'; // Ensure we draw in exact center + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; + startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise; + } + ctx.rotate(startAngle); + for (x = 0; x < text.length; x++) { + cw = ctx.measureText(text[x]).width; // half letter + ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise); + if (this.strokeStyle && this.strokeWidth) { + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.miterLimit = 2; + ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + } + ctx.fillStyle = fill; + ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2)); + ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter + } + return canvas; + }, + _set: function (key, value) { + switch (key) { + case 'scaleX': + this.fontSize *= value; + this.diameter *= value; + this.width *= value; + this.scaleX = 1; + if (this.width < 1) { this.width = 1; } + break; + case 'scaleY': + this.height *= value; + this.scaleY = 1; + if (this.height < 1) { this.height = 1; } + break; + default: + this.callSuper('_set', key, value); + break; + } + }, + _render: function (ctx) { + var canvas = this.getCircularText(); + this.cropCanvas(canvas); + this.set('width', canvas.width); + this.set('height', canvas.height); + ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height); + this.setCoords(); + }, + toObject: function (propertiesToInclude) { + return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude)); + } + }); + fabric.CurvedText.fromObject = function (object, callback, forceAsync) { + return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text'); + }; + })(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric); + + const remote = require('@electron/remote') + var path = require('path'); + // 使用用户数据目录,避免写入需要管理员权限 + const exePath = remote.app.getPath('userData') + const { ipcRenderer } = require('electron') + const { dialog } = require('@electron/remote') + var jrQrcode = require('jr-qrcode') + var JsBarcode = require('jsbarcode') + ipcRenderer.send('get-sys-fonts') + ipcRenderer.send('get-scale-rate') + const { clipboard } = require('electron') + const dpi = 300 + // 内部剪贴板 + let clipboardData = { + objects: null, + offset: 10 // 粘贴位置偏移量 + }; + //var encrypt = new JSEncrypt(); + //encrypt.setPublicKey(`-----BEGIN PUBLIC KEY----- + //MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDAbKQ46WevL9PapJ+AlZqSzczkTovu7STH + //FmZmIJcNrwPPEWRR2PliUvtS28V+gNMWlu3wNWFAS5KGDbTtndIrP3hnlp9BDIcAcdNuJUM7 + //yJfAYBhtkqvj45Orvdd2RYRxhkQb34uTNXpM+biMH6Wd0wykNb9/VBNM0C12KswEdwIBEQ== + //-----END PUBLIC KEY-----`); + layui.use(['layer', 'slider', 'form', 'colorpicker'], function () { + let myDate = new Date() + let s_lan = '' + ipcRenderer.send('get-sys-language') + /* if(myDate.getFullYear()>2021 && myDate.getMonth() >= 1 && myDate.getDate() > 15){ + + }*/ + function GetFile() { + const params = new URLSearchParams(window.location.search); + return params + } + function langua_ge(lan = 'zh') { + $("[language='m']").each(function (i) { + $(this).html($(this).attr(lan)) + }) + + $("[language='t']").each(function (i) { + $(this).attr('title', $(this).attr(lan)) + }) + } + function language_str(str) { + let t = { + whetherSave: { zh: '是否保存当前文件?', ozh: '是否保存當前文件?', en: 'Do you want to save the current file?' }, + save: { zh: '保存', ozh: '保存', en: 'Save' }, + noSave: { zh: '不保存', ozh: '不保存', en: 'No saving' }, + cancel: { zh: '取消', ozh: '取消', en: 'Cancel' }, + saveSucc: { zh: '保存成功至', ozh: '保存成功至', en: 'Save successfully to' }, + saveFile: { zh: '保存文件', ozh: '保存文件', en: 'Save file' }, + display: { zh: '预览', ozh: '預覽', en: 'Display' }, + output: { zh: '导出', ozh: '導出', en: 'Export' }, + bg: { zh: '背景', ozh: '背景', en: 'Background' }, + selectPic: { zh: '请选择图片', ozh: '請選擇圖片', en: 'Please select a picture' }, + selectPic: { zh: '请选择文件', ozh: '請選擇文件', en: 'Please select a file' }, + img: { zh: '圖片', ozh: '圖片', en: 'Image' }, + simg: { zh: '合成圖片', ozh: '合成圖片', en: 'Synthesized Image' }, + text: { zh: '文本', ozh: '文本', en: 'Text' }, + "ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" }, + stext: { zh: '合成文本', ozh: '合成文本', en: 'Synthesized Text' }, + rect: { zh: '矩形', ozh: '矩形', en: 'Rectangle' }, + circ: { zh: '圓形', ozh: '圓形', en: 'Circle' }, + line: { zh: '直線', ozh: '直線', en: 'Line' }, + qrc: { zh: '二維碼', ozh: '二維碼', en: 'QR code' }, + barc: { zh: '條形碼', ozh: '條形碼', en: 'Barcode' }, + counter: { zh: '計數器', ozh: '计数器', en: 'Counter' }, + about: { zh: '关于Soon Design', ozh: '關於Soon Design', en: 'About Soon Design' }, + vers: { zh: '版本', ozh: '版本', en: 'Version' }, + comf: { zh: '确认', ozh: '確認', en: 'Select' }, + copyright: { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." } + } + return t[str][s_lan] + } + + var $ = layui.$ + var layer = layui.layer, + slider = layui.slider, + colorpicker = layui.colorpicker + let addState = 0 //0是不添加 + let background_image, background_image1, background_image2 + let bg_version = 1 //front_bg1.png front_bg2.png + var zoom = 0 + $('#canvas1').attr('height', document.body.clientHeight - 100) + $('#canvas2').attr('height', document.body.clientHeight - 100) + $('#object_attribute').css('height', '470px') + $('#obj_list').css('height', document.body.clientHeight - 520) + $('.obj_list').css('height', $('#obj_list').height() - 32) + + $('.main').show() + $('#canvas1').attr('width', $('#canvas-div').width()) + $('#canvas2').attr('width', $('#canvas-div').width()) + var openAs = { + _name: '', + set name(val) { + if (val == '') { + $('title').html('Soon Design') + } else { + $('title').html('Soon Design - ' + val) + } + this._name = val + }, + get name() { + return this._name + } + } //被打开的文件名 + let objs1 = [] + let objs2 = [] + let step1 = { val: 0 }, + step2 = { val: 0 } + let step = step1 + let objs = objs1 + let recordJson1 = [], + recordJson2 = [], + recordJson = recordJson1, + recordObjs1 = [], + recordObjs2 = [], + recordObjs = recordObjs1 + let pre_objs1 = [], + pre_objs2 = [], + next_objs1 = [], + next_objs2 = [], + pre_objs = pre_objs1, + next_objs = next_objs1 + let pre_json1 = [], + pre_json2 = [], + next_json1 = [], + next_json2 = [], + pre_json = pre_json1, + next_json = next_json1 + var canvas1 = new fabric.Canvas('canvas1') + var canvas2 = new fabric.Canvas('canvas2') + fabric.Object.prototype.objectCaching = false + var ctx1 = canvas1.getSelectionContext() + var ctx2 = canvas2.getSelectionContext() + let is_bgi_add = false + canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) + //console.log(canvas1.width / 2, canvas1.height / 2); + //console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200); + canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200) + zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200 + + // 初始化标尺 + initRulers(); + $('#source_front').width(zoom * $('#source_front').width()) + $('#source_back').width(zoom * $('#source_back').width()) + $('.canvas-container:eq(1)').hide() + var canvas = canvas1 + fabric.Object.prototype.set({ + borderColor: '#9013FE', + cornerColor: '#9013FE', //激活状态角落图标的填充颜色 + cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色 + borderOpacityWhenMoving: 1, + borderScaleFactor: 1, + cornerSize: 6, + cornerStyle: 'circle', //rect,circle + centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点 + centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转 + transparentCorners: false, //激活状态角落的图标是否透明 + rotatingPointOffset: 20, //旋转距旋转体的距离 + padding: 0 + }) + canvas1.preserveObjectStacking = true + canvas2.preserveObjectStacking = true + + var front_list = [] + ipcRenderer.on('font-list', (event, data) => { + const newFontList = [] + data.map((item) => { + if (item.indexOf('"') === 0) { + newFontList.push(item.replace(/^"|"$/g, '')) + } else { + newFontList.push(item) + } + }) + front_list = newFontList + for (let item of front_list) { + $('#text_font_family').append("') + $("#circle_text_font_family").append(""); + $('#out_text_font_family').append("') + $('#barcode_font_family').append("') + $('#count_font_family').append("') + } + }) + ipcRenderer.on('sys-lan', (event, data) => { + let lang = localStorage.getItem('lang') + if (lang) { + s_lan = lang + langua_ge(lang) + $('#language_select').val(lang) + return + } + switch (data) { + case 'zh-CN': + s_lan = 'zh' + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh': + s_lan = 'zh' + langua_ge('zh') + $('#language_select').val('zh') + localStorage.setItem('lang', 'zh') + break + case 'zh-TW': + s_lan = 'ozh' + langua_ge('ozh') + $('#language_select').val('ozh') + localStorage.setItem('lang', 'ozh') + break + case 'en': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-AU': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-CA': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-GB': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-NZ': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-US': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + case 'en-ZA': + s_lan = 'en' + langua_ge('en') + $('#language_select').val('en') + localStorage.setItem('lang', 'en') + break + } + }) + ipcRenderer.on('close', (event, message) => { + layer.confirm( + '' + language_str('whetherSave') + '', + { + //是否保存当前文件? + btn: [language_str('save'), language_str('noSave'), language_str('cancel')], //['保存', '不保存', '取消'] + btn3: function (index, layero) { } + }, + function (index, layero) { + output(() => ipcRenderer.send('run-close')) + return + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + + ipcRenderer.send('run-close') + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + ipcRenderer.send('run-close') + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //保存成功至 + saveHistory() + ipcRenderer.send('run-close') + }) + .catch((err) => { + console.log() + }) + layer.close(index) + }, + function (index) { + ipcRenderer.send('run-close') + } + ) + }) + + addBackground() //添加背景 白色卡片 + + initAligningGuidelines(canvas1) + initAligningGuidelines(canvas2) + //canvas.setZoom(0.5) + //recordObjs1.push(JSON.stringify(objs1)); + //recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //recordObjs2.push(JSON.stringify(objs2)); + //recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + canvas.renderAll() //重新渲染画布 + var zoomSlider = slider.render({ + elem: '#zoomSlide', //绑定元素 + min: 25, + max: 500, + value: 100, + change(val) { + canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100) + zoom = val / 100 + //$(".canvas-container").css("transform","scale(" + val / 100 + ")") + } + }) + window.rotate = function (id) { + let angle = $(`#${id}`).attr('angle') - 0 + angle += 90 + $(`#${id}`).css('transform', `rotate(${angle}deg)`) + $(`#${id}`).attr('angle', angle) + } + $('#display').on('click', function () { + // 类型改变 + fabric.Image.fromURL('./public/images/front_bg' + bg_version + '_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + + fabric.Image.fromURL('./public/images/back_bg_2.png', function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + fabric.Image.fromURL('./public/images/op_2.png', function (i3) { + i2.left = background_image.left + i2.top = background_image.top + let img3 = i3 + display_func(img1, img2, img3) + }) + }) + }) + }) + // 导出文件 + async function savePdf(buffer) { + let fs = require('fs'); + // 弹出保存对话框 + const { canceled, filePath } = await dialog.showSaveDialog({ + title: '保存文件', + defaultPath: path.join(exePath, 'output.pdf'), // 默认路径 + filters: [{ name: 'PDF 文件', extensions: ['pdf'] }] // 文件过滤器 + }); + + if (canceled) { + console.log('用户取消了保存操作'); + return; + } + + // 将Buffer写入PNG文件到用户选择的路径 + fs.writeFile(filePath, buffer, (err) => { + if (err) { + console.error('写入文件失败:', err); + } else { + layer.msg(language_str("saveSucc") + filePath);//'保存成功至' + console.log('PNG文件已保存至:', filePath); + } + }); + } + function display_func(img1, img2, img3) { + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $('#circle_text_control').hide() + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + let _objs1 = canvas1.getObjects() + let g1 = [] + let g3 = [] + let g5 = [] + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + + // g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let _objs2 = canvas2.getObjects() + let g2 = [] + let g4 = [] + let g6 = [] + left = 0 + top = 0 + black_top = 0 + black_left = 0 + all_top = 0 + all_left = 0 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //黑色字或计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if (idx != 0 && objs2[idx - 1].type == 10) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + // g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + let fs = require('fs'); + const Buffer = require('buffer').Buffer; + const buffer3 = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + const printPath3 = path.join(exePath, 'print3.png'); + fs.writeFileSync(printPath3, buffer3); + const buffer4 = Buffer.from(url4.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + const printPath4 = path.join(exePath, 'print4.png'); + fs.writeFileSync(printPath4, buffer4); + // 创建 PDF 实例 + const pdf = new jspdf.jsPDF({ + orientation: 'landscape', + unit: 'px', + format: [1012, 648] + }); + let btns = [language_str('output')] + if ((objs1.length != 0) & (objs2.length == 0)) { + //只有正面 + let img = new Image() + img.src = printPath3 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + btns.push('打印正面') + } else if ((objs1.length == 0) & (objs2.length != 0)) { + //只有背面 + let img = new Image() + img.src = printPath4 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + btns.push('打印背面') + } else { + let img = new Image() + img.src = printPath3 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + + pdf.addPage() + let img2 = new Image() + img2.src = printPath4 + pdf.addImage(img2, 'JPEG', 0, 0, 1012, 648) + btns.push('打印正面') + btns.push('打印背面') + } + + var pdfBlob = pdf.output('blob') + var pdfBuffer = pdf.output('arraybuffer') + + layer.open({ + type: 1, + area: [w, h], + title: language_str('display'), //不显示标题栏"预览" + shadeClose: true, //点击遮罩关闭 + content: ` +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    `, + btn: btns, //'导出' + btn1: function (index, layero) { + savePdf(Buffer.from(pdfBuffer)) + }, + btn2: function () { + if (btns[1] === '打印正面') { + printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } else { + printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } + }, + btn3: function () { + //打印背面 + printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }) + } + }) + } + function output(callback = null, _save = save) { + // 类型改变 + fabric.Image.fromURL('./public/images/op_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + fabric.Image.fromURL('./public/images/op_2.png', function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + let _objs1 = canvas1.getObjects() + let g1 = [] + let g3 = [] + let g5 = [] //黑色图层 + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && item.get('fill') == '#000000') || + ( + objs1[idx - 1].type == 11 + && + item.get("fill") == "#000000" + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // || + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let _objs2 = canvas2.getObjects() + let g2 = [] + let g4 = [] + let g6 = [] //黑色图层 + left = 0 + top = 0 + black_left = 0 //黑色左裁剪 + black_top = 0 //黑色顶裁剪 + all_left = 0 //黑色左裁剪 + all_top = 0 //黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } + if ((objs1.length != 0) & (objs2.length == 0)) { + op.flag = 2 + } else if ((objs1.length == 0) & (objs2.length != 0)) { + op.flag = 3 + } + op.frontColorPic = desEncrypt(url1) + op.backColorPic = desEncrypt(url2) + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = desEncrypt(url5) + op.backBlackPic = desEncrypt(url6) + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + if (idx == 0) continue //background-image + let type = objs1[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 1, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 3, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontBackgroudColor: item.get('textBackgroundColor'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + Exceed: objs1[idx - 1].exceed, + AutoWrap: objs1[idx - 1].autoWrap, + FixSize: objs1[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs1[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 4, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].color, + DefaultText: objs1[idx - 1].val, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 5, + name: objs1[idx - 1].name, + FontSize: parseInt(objs1[idx - 1].fontSize), + DefaultText: objs1[idx - 1].val, + HideText: objs1[idx - 1].show, + FontFamily: objs1[idx - 1].font, + FontColor: objs1[idx - 1].color, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 6, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: objs1[idx - 1].start, + add: parseInt(objs1[idx - 1].add), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (idx == 0) continue //background-image + let type = objs2[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 1, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 3, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontSize: parseInt(item.get('fontSize')), + FontBackgroudColor: item.get('textBackgroundColor'), + FontColor: item.get('fill'), + Exceed: objs2[idx - 1].exceed, + AutoWrap: objs2[idx - 1].autoWrap, + FixSize: objs2[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs2[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 4, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].color, + DefaultText: objs2[idx - 1].val, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 5, + name: objs2[idx - 1].name, + DefaultText: objs2[idx - 1].val, + HideText: objs1[idx - 1].show, + FontSize: parseInt(objs2[idx - 1].fontSize), + FontFamily: objs2[idx - 1].font, + FontColor: objs2[idx - 1].color, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 6, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: parseInt(objs2[idx - 1].start), + add: parseInt(objs2[idx - 1].add), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + _save(op, callback) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + } + $('#output').on('click', function () { + output(null, saveAs) + return + // 类型改变 + fabric.Image.fromURL('./public/images/op_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + fabric.Image.fromURL('./public/images/op_2.png', function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + let _objs1 = canvas1.getObjects() + let g1 = [] + let g3 = [] + let g5 = [] //黑色图层 + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let _objs2 = canvas2.getObjects() + let g2 = [] + let g4 = [] + let g6 = [] //黑色图层 + left = 0 + top = 0 + black_left = 0 //黑色左裁剪 + black_top = 0 //黑色顶裁剪 + all_left = 0 //黑色左裁剪 + all_top = 0 //黑色顶裁剪 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type ==11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } + if ((objs1.length != 0) & (objs2.length == 0)) { + op.flag = 2 + } else if ((objs1.length == 0) & (objs2.length != 0)) { + op.flag = 3 + } + op.frontColorPic = desEncrypt(url1) + op.backColorPic = desEncrypt(url2) + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = desEncrypt(url5) + op.backBlackPic = desEncrypt(url6) + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + if (idx == 0) continue //background-image + let type = objs1[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 1, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs1[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 3, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontBackgroudColor: item.get('textBackgroundColor'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + Exceed: objs1[idx - 1].exceed, + AutoWrap: objs1[idx - 1].autoWrap, + FixSize: objs1[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs1[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 4, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].color, + DefaultText: objs1[idx - 1].val, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 5, + name: objs1[idx - 1].name, + FontSize: parseInt(objs1[idx - 1].fontSize), + DefaultText: objs1[idx - 1].val, + HideText: objs1[idx - 1].show, + FontFamily: objs1[idx - 1].font, + FontColor: objs1[idx - 1].color, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 6, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: objs1[idx - 1].start, + add: parseInt(objs1[idx - 1].add), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + //obj2 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + if (idx == 0) continue //background-image + let type = objs2[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 1, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)), + "circleCenter" : objs2[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 3, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontSize: parseInt(item.get('fontSize')), + FontBackgroudColor: item.get('textBackgroundColor'), + FontColor: item.get('fill'), + Exceed: objs2[idx - 1].exceed, + AutoWrap: objs2[idx - 1].autoWrap, + FixSize: objs2[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs2[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 4, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].color, + DefaultText: objs2[idx - 1].val, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 5, + name: objs2[idx - 1].name, + DefaultText: objs2[idx - 1].val, + HideText: objs1[idx - 1].show, + FontSize: parseInt(objs2[idx - 1].fontSize), + FontFamily: objs2[idx - 1].font, + FontColor: objs2[idx - 1].color, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 6, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: parseInt(objs2[idx - 1].start), + add: parseInt(objs2[idx - 1].add), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'CS File Type', extensions: ['cs'] }] + }) + .then((result) => { + if (result.canceled) return + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.cs' + } + let fs = require('fs') + op.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(op)) + }) + .catch((err) => { + console.log() + }) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) + }) + var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false + $('input').focus(function () { + console.log(1) + _inputEdit = true + }) + $('input').blur(function () { + _inputEdit = false + }) + $('textarea').focus(function () { + console.log(1) + _inputEdit = true + }) + $('textarea').blur(function () { + _inputEdit = false + }) + $('body').keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false + } + if (event.keyCode == 67) { + _keyc = false + } + }) + $('body').keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + if (!_inputEdit && !_textEdit) { + // CTRL/CMD+N - 新建 + if (isModifierKey && event.keyCode == 78) { + event.preventDefault(); + $('#new').click(); + return; + } + // CTRL/CMD+O - 打开 + if (isModifierKey && event.keyCode == 79) { + event.preventDefault(); + $('#open').click(); + return; + } + // CTRL/CMD+S - 保存 + if (isModifierKey && !event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + $('#save').click(); + return; + } + // CTRL/CMD+SHIFT+S - 保存副本 + if (isModifierKey && event.shiftKey && event.keyCode == 83) { + event.preventDefault(); + output(null, saveAs); + return; + } + // CTRL/CMD+Z - 还原 + if (isModifierKey && !event.shiftKey && event.keyCode == 90) { + event.preventDefault(); + $('#previous').click(); + return; + } + // CTRL/CMD+Y - 重做 + if (isModifierKey && !event.shiftKey && event.keyCode == 89) { + event.preventDefault(); + $('#next').click(); + return; + } + // CTRL/CMD+C - 复制 + if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') { + event.preventDefault(); + copySelection(); + return; + } + // CTRL/CMD+V - 粘贴 + if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') { + event.preventDefault(); + pasteFromClipboard(); + return; + } + // CTRL/CMD+SHIFT+DEL - 清空 + if (isModifierKey && event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#clean').click(); + return; + } + // CTRL/CMD+DEL - 删除 + if (isModifierKey && !event.shiftKey && event.keyCode == 46) { + event.preventDefault(); + $('#delete').click(); + return; + } + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 67) { + _keyc = true + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + //clipboard.writeText((clipboard.readText()+' ')) + } + } + if (event.keyCode == 37) { + //left + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + if (event.keyCode == 38) { + //up + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + if (event.keyCode == 39) { + //right + let v = 1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + if (event.keyCode == 40) { + //down + let v = +1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + updateList() + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + handleObjectSelected() + } + } + }) + let pre_add_image + $('#addPic').click(function () { + OpenDialog() + function OpenDialog() { + dialog + .showOpenDialog({ + title: language_str('selectPic'), //请选择图片 + buttonLabel: language_str('comf'), //"确认" + filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] + }) + .then((result) => { + if (result.canceled) return + //console.log(result.filePaths) + fabric.Image.fromURL(result.filePaths[0], function (image) { + //image.left = pointer.x; + //image.top = pointer.y; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 1 + }) + //canvas.add(image); + //objs.push({type: 1}); + //canvas.renderAll(); + //updateList(); + //recordState(); + }) + }) + .catch((err) => { + console.log(err) + }) + } + }) + $('#addOutPic').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 2 + }) + $('#addText').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 3 + }) + $("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; + }); + $('#addOutText').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 4 + }) + $('#addRect').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 5 + }) + $('#addCircle').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 6 + }) + $('#addLine').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 7 + }) + $('#addQrCode').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 8 + }) + $('#addBarCode').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 9 + }) + $('#addCounter').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + addState = 10 + }) + $('#version_change').click(function () { + if (bg_version == 1) { + bg_version = 2 + } else { + bg_version = 1 + } + // 类型改变 + $('#source_front').attr('src', './public/images/front_bg' + bg_version + '_2.png') + canvas.renderAll() + }) + $('#front_side').click(function () { + $(this).addClass('ui-button-active') + $('#back_side').removeClass('ui-button-active') + $('.canvas-container:eq(0)').show() + $('.canvas-container:eq(1)').hide() + console.log('------') + console.log(recordObjs1) + console.log('------') + canvas = canvas1 + objs = objs1 + background_image = background_image1 + next_objs = next_objs1 + pre_objs = pre_objs1 + next_json = next_json1 + pre_json = pre_json1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + updateList() + + $('#line_control').hide() + $('#base_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //背景 + }) + $('#back_side').click(function () { + $(this).addClass('ui-button-active') + $('#front_side').removeClass('ui-button-active') + $('.canvas-container:eq(0)').hide() + $('.canvas-container:eq(1)').show() + canvas = canvas2 + objs = objs2 + background_image = background_image2 + next_objs = next_objs2 + pre_objs = pre_objs2 + next_json = next_json2 + pre_json = pre_json2 + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + updateList() + $('#line_control').hide() + $('#base_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + }) + $('#top_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set('top', background_image.get('top')) + canvas.renderAll() + return + } + let top = 0 + for (let item of _objs) { + let t_top = item.get('top') + if (t_top < top) { + top = t_top + } + } + groupItems = [] + for (let item of _objs) { + item.set('top', top) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#bottom_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get('strokeWidth') + } + _objs[0].set('top', background_image.get('top') + background_image.get('height') - _objs[0].get('height') * _objs[0].get('scaleY') - other) + canvas.renderAll() + return + } + let bottom = 0 + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + let t_b = item.get('top') + item.get('height') * item.get('scaleY') + other + if (t_b > bottom) { + bottom = t_b + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('top', bottom - item.get('height') * item.get('scaleY') - other) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#center_y_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get('strokeWidth') + } + _objs[0].set('top', background_image.get('top') + (background_image.get('height') - (_objs[0].get('height') + other) * _objs[0].get('scaleY')) / 2) + canvas.renderAll() + return + } + let top = 0 + for (let item of _objs) { + let t_top = item.get('top') + if (t_top < top) { + top = t_top + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('top', top + (item.group.height - (item.get('height') + other) * item.get('scaleY')) / 2) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#left_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + _objs[0].set('left', background_image.get('left')) + canvas.renderAll() + return + } + let left = 0 + for (let item of _objs) { + let t_left = item.get('left') + if (t_left < left) { + left = t_left + } + } + groupItems = [] + for (let item of _objs) { + item.set('left', left) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#right_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + _objs[0].set('left', background_image.get('left') + background_image.get('width') - _objs[0].get('width') * _objs[0].get('scaleX') - other) + canvas.renderAll() + return + } + let right = 0 + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + let t_r = item.get('left') + item.get('width') * item.get('scaleX') + other + if (t_r > right) { + right = t_r + } + } + let groupItems = [] + + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('left', right - item.get('width') * item.get('scaleX') - other) + groupItems.push(item) + } + //BUG1 + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#center_x_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个 + let idx = getIndex(_objs[0]) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = _objs[0].get('strokeWidth') + } + _objs[0].set('left', background_image.get('left') + (background_image.get('width') - (_objs[0].get('width') + other) * _objs[0].get('scaleX')) / 2) + canvas.renderAll() + return + } + let left = 0 + for (let item of _objs) { + let t_left = item.get('left') + if (t_left < left) { + left = t_left + } + } + groupItems = [] + for (let item of _objs) { + let idx = getIndex(item) + let type = objs[idx - 1].type + let other = 0 //strokeWidth + if (type == 5 || type == 6 || type == 7) { + other = item.get('strokeWidth') + } + item.set('left', left + (item.group.width - (item.get('width') + other) * item.get('scaleX')) / 2) + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + }) + $('#up_obj').click(function () { + //上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()) + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()) + canvas.renderAll() + let temp = objs[idx - 1] + objs[idx - 1] = objs[idx] + objs[idx] = temp + //console.log(objs); + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + recordState() + updateList() + selectObj(idx) + } + }) + $('#down_obj').click(function () { + //下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()) + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()) + canvas.renderAll() + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1] + objs[idx - 1] = objs[idx - 2] + objs[idx - 2] = temp + recordState() + updateList() + selectObj(idx - 2) + } + }) + $('#set_bg').click(function () { + //设置为背景 + let obj = canvas.getActiveObject() + let idx = getIndex(canvas.getActiveObject()) + const scaleX = 1012 / obj.width + const scaleY = 648 / obj.height + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - 1012) / 2, + top: ($('#canvas1').height() - 648) / 2 + }) + $('#text_w').val(1012) + $('#text_h').val(648) + $('#text_x').val(0) + $('#text_y').val(0) + canvas.sendToBack(canvas.getActiveObject()) + canvas.bringForward(canvas.getActiveObject()) + canvas.renderAll() + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + recordState() + updateList() + selectObj(0) + }) + $('#previous').click(function () { + //撤销一次操作 + if (step.val == 0) { + return + } + + step.val-- + console.log(recordJson[step.val]) + canvas.loadFromJSON(recordJson[step.val]) + if ($('#front_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs1 = JSON.parse(recordObjs[step.val]) + objs = objs1 + } else if ($('#back_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs2 = JSON.parse(recordObjs[step.val]) + objs = objs2 + } + + background_image = canvas.getObjects()[0] + canvas.renderAll() + //recordState(); + updateList() + + selectObj(-1) + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + }) + $('#next').click(function () { + //撤销一次撤销操作 + if (step.val >= recordObjs.length - 1) { + return + } + + ++step.val + canvas.loadFromJSON(recordJson[step.val]) + if ($('#front_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs1 = JSON.parse(recordObjs[step.val]) + objs = objs1 + } else if ($('#back_side').hasClass('ui-button-active')) { + //objs = JSON.parse(recordObjs[step.val]); + objs2 = JSON.parse(recordObjs[step.val]) + objs = objs2 + } + background_image = canvas.getObjects()[0] + canvas.renderAll() + selectObj(-1) + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + updateList() + }) + $('#clean').click(function () { + objs.splice(0, objs.length) + + let allObjs = canvas.getObjects() + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item) + } + } + updateList() + recordState() + }) + $('#rotate').click(function () { + let obj = canvas.getActiveObject() + obj.rotate(Math.round((obj.get('angle') + 90) % 360)) + $('#text_r').val(obj.get('angle')) + canvas.renderAll() + updateControls() + recordState() + }) + $('#text_italics').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('fontStyle', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontStyle', 'italic') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#text_bold').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选,恢复正常 + canvas.getActiveObject().set('fontWeight', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontWeight', 'bold') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#text_underline').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('underline', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('underline', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#text_linethrough').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('linethrough', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('linethrough', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); + }); + $('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + recordState() + canvas.renderAll() + }) + $('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + recordState() + canvas.renderAll() + }) + $('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + recordState() + canvas.renderAll() + }) + $('#out_text_italics').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('fontStyle', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontStyle', 'italic') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#out_text_bold').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选,恢复正常 + canvas.getActiveObject().set('fontWeight', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontWeight', 'bold') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#out_text_underline').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('underline', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('underline', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#out_text_linethrough').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('linethrough', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('linethrough', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() + }) + $('#lock').click(function () { + canvas.getActiveObject().set('selectable', !canvas.getActiveObject().get('selectable')) + if (canvas.getActiveObject().get('selectable')) { + $('#lock_img').attr('src', 'public/images/unlock20.png') //已经上锁,需要解锁 + } else { + $('#lock_img').attr('src', 'public/images/lock20.png') //没上锁,可以上锁 + } + recordState() + canvas.renderAll() + }) + $('#text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#text_back_color').val('#ffffff') + canvas.renderAll() + }) + $('#out_text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#out_text_back_color').val('#ffffff') + canvas.renderAll() + }) + $('#rect_transparent').click(function () { + canvas.getActiveObject().set('fill', '') + $('#rect_color').val('#ffffff') + canvas.renderAll() + }) + $('#rect_stroke_transparent').click(function () { + canvas.getActiveObject().set('stroke', '') + $('#rect_stroke').val('#ffffff') + canvas.renderAll() + }) + $('#circle_transparent').click(function () { + canvas.getActiveObject().set('fill', '') + $('#circle_color').val('#ffffff') + canvas.renderAll() + }) + $('#circle_stroke_transparent').click(function () { + canvas.getActiveObject().set('stroke', '') + $('#circle_stroke').val('#ffffff') + canvas.renderAll() + }) + function saveAs(op1, callback) { + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + let con_o = Object.assign(o, op1) + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + if (callback && typeof callback === 'function') { + callback() + } + }) + .catch((err) => { + console.log() + }) + } + function save(op1, callback) { + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + let con_o = Object.assign(o, op1) + console.log(op1) + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(con_o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + if (callback && typeof callback === 'function') { + callback() + } + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + if (callback && typeof callback === 'function') { + callback() + } + }) + .catch((err) => { + console.log() + }) + } + // 复制 + $("#copy").click(function () { + copySelection(); + }) + // 粘贴 + $("#paste").click(function () { + pasteFromClipboard(); + }) + // 保存 + $('#save').click(function () { + output() + return + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + let con_o = Object.assign(o, op1) + console.log(op1) + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(con_o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + con_o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(con_o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + saveHistory() + }) + .catch((err) => { + console.log() + }) + }) + function getDate() { + let date = new Date() + return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + } + function getAbsoluteXY(_obj) { + let coord = _obj.get('lineCoords') + //let xs = [], ys = []; + //for(let item in coord){ + // xs.push(coord[item].x); + // ys.push(coord[item].y); + //} + //xs.sort(function(a,b){return a-b}); + //ys.sort(function(a,b){return a-b}); + //return [xs[0], ys[0]]; + return [_obj.get('left'), _obj.get('top')] + } + let fullPath = path.join(exePath, 'data.json') + function saveHistory() { + let fs = require('fs') + let j = {} + try { + fsData = fs.readFileSync(fullPath) + j = JSON.parse(fsData.toString()) + for (let item of j.history) { + if (item.path == openAs.name) { + item.time = getDate() + fs.writeFileSync(fullPath, JSON.stringify(j)) + return + } + } + j.history.push({ time: getDate(), path: openAs.name }) + fs.writeFileSync(fullPath, JSON.stringify(j)) + } catch (e) { + let j = { history: { time: getDate(), path: openAs.name } } + fs.writeFileSync(fullPath, JSON.stringify(j)) + } + } + function open(file) { + let fs = require('fs') + var fsData = fs.readFileSync(file) + openAs.name = file + let j = JSON.parse(fsData.toString()) + let left = background_image.left + let top = background_image.top + let new_left = j.f.objects[0].left + let new_top = j.f.objects[0].top + for (let o of j.f.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + for (let o of j.b.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + canvas1.loadFromJSON(j.f) + canvas2.loadFromJSON(j.b) + background_image1 = canvas1.getObjects()[0] + background_image2 = canvas2.getObjects()[0] + + objs2 = j.bo + objs1 = j.fo + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + background_image = background_image1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + background_image = background_image2 + step = step2 + } + canvas1.renderAll() + canvas2.renderAll() + updateList() /* + 修复打开新文件后无法撤销的bug + 2022-07-16 + */ + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j1.objects[0].hoverCursor = 'default' + recordJson1.push(j1) + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j2.objects[0].hoverCursor = 'default' + recordJson2.push(j2) + }, 0) + } + $('#open').click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm( + '' + language_str('whetherSave') + '', + function (index) { + //confirm 是否保存当前文件? + //let j = canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + //let b = canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + //let o = { f : j, b : b,fo : objs1, bo : objs2}; + //if(openAs != ""){ + // //打开的文件 + // let fs = require('fs') + // fs.writeFileSync(openAs,JSON.stringify(o)) + // layer.msg('保存成功至'+ openAs); + // + // OpenDialog();return; + //} + //dialog.showSaveDialog({ + // title:'保存文件', + // filters: [ + // { name: 'FCS File Type', extensions: ['fcs'] }, + // ] + //}).then(result=>{ + // let fs = require('fs') + // fs.writeFileSync(result.filePath,JSON.stringify(o)) + // + // OpenDialog(); + //}).catch(err=>{ + // console.log() + //}) + /* + let j = canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + let b = canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]); + let _g1 = [], _g2 = []; + let left = 0, top = 0; + for( let _o of canvas1.getObjects() ){ + if( getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left){ + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if( getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top){ + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g1.push(fabric.util.object.clone(_o)); + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top:top/zoom, + left:left/zoom + }); + left = 0, top = 0; + for( let _o of canvas2.getObjects() ){ + if( getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left){ + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())); + } + if( getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top){ + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())); + } + _g2.push(fabric.util.object.clone(_o)); + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top:top/zoom, + left:left/zoom + }); + let o = { f : j, b : b,fo : objs1, bo : objs2,fp:fp,bp:bp}; + if(openAs.name != ""){ + //打开的文件 + let fs = require('fs') + fs.writeFileSync(openAs.name,JSON.stringify(o)) + layer.msg(language_str("saveSucc")+ openAs.name);//'保存成功至' + saveHistory(); + + OpenDialog(); + return; + } + dialog.showSaveDialog({ + title:language_str("saveFile"),//'保存文件' + filters: [ + { name: 'Soon File Type', extensions: ['soon'] }, + ] + }).then(result=>{ + if(result.canceled)return; + if(result.filePath.substring(result.filePath.length-5).indexOf('.') == -1){ + result.filePath += '.soon'; + } + let fs = require('fs') + fs.writeFileSync(result.filePath,JSON.stringify(o)) + openAs.name = result.filePath; + layer.msg(language_str("saveSucc")+ openAs.name);//'保存成功至' + saveHistory(); + OpenDialog(); + }).catch(err=>{ + console.log() + }) + */ + output(OpenDialog) + layer.close(index) + }, + function (index) { + OpenDialog() + } + ) + function OpenDialog() { + dialog + .showOpenDialog({ + title: '请选择文件', //confirm + buttonLabel: language_str('comf'), //"确认" + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.canceled) return + //console.log(result.filePaths) + let fs = require('fs') + var fsData = fs.readFileSync(result.filePaths[0]) + openAs.name = result.filePaths[0] + let j = JSON.parse(fsData.toString()) + let type = j.soonType ? j.soonType : j.backBlackPic ? 2 : 1; + if (type == 1) { + ipcRenderer.send('open-design-page', openAs.name, type); + return + } + let left = background_image.left + let top = background_image.top + let new_left = j.f.objects[0].left + let new_top = j.f.objects[0].top + for (let o of j.f.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + for (let o of j.b.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + canvas1.loadFromJSON(j.f) + canvas2.loadFromJSON(j.b) + background_image1 = canvas1.getObjects()[0] + background_image2 = canvas2.getObjects()[0] + + objs2 = j.bo + objs1 = j.fo + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + background_image = background_image1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + background_image = background_image2 + step = step2 + } + canvas1.renderAll() + canvas2.renderAll() + updateList() + /* + 修复打开新文件后无法撤销的bug + 2022-07-16 + */ + setTimeout(() => { + recordObjs1.push(JSON.stringify(objs1)) + let j1 = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j1.objects[0].hoverCursor = 'default' + recordJson1.push(j1) + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j2.objects[0].hoverCursor = 'default' + recordJson2.push(j2) + }, 0) + }) + .catch((err) => { + console.log(err) + }) + } + }) + function clearAll() { + //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; + //let j = JSON.parse(blank); + //canvas1.setZoom(1); + //canvas2.setZoom(1); + //canvas1.loadFromJSON(j.f); + //canvas2.loadFromJSON(j.b); + let t_objs1 = canvas1.getObjects() + let t_objs2 = canvas2.getObjects() + for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]) + } + for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]) + } + objs2 = [] + objs1 = [] + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + } + openAs.name = '' + canvas1.renderAll() + canvas2.renderAll() + updateList() + console.log() + } + $('#new').click(function () { + if (step.val == 0) { + ipcRenderer.send('open-first-page'); + return; + } + layer.confirm( + '' + language_str('whetherSave') + '', + function (index) { + //confirm 是否保存当前文件? + output(() => ipcRenderer.send('open-first-page')) + return + let j = canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let b = canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + let _g1 = [], + _g2 = [] + let left = 0, + top = 0 + for (let _o of canvas1.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g1.push(fabric.util.object.clone(_o)) + } + let fp = new fabric.Group(_g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + ; (left = 0), (top = 0) + for (let _o of canvas2.getObjects()) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(_o.getCoords()) > left) { + left = -(getCoordsMinX(_o.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(_o.getCoords()) > top) { + top = -(getCoordsMinY(_o.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + _g2.push(fabric.util.object.clone(_o)) + } + //let fp = new fabric.Group(_g1).toDataURL(); + let bp = new fabric.Group(_g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let o = { f: j, b: b, fo: objs1, bo: objs2, fp: fp, bp: bp } + if (openAs.name != '') { + //打开的文件 + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(openAs.name, JSON.stringify(o)) + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page') + //saveHistory(); + //clearAll(); + return + } + dialog + .showSaveDialog({ + title: language_str('saveFile'), //'保存文件' + filters: [{ name: 'Soon File Type', extensions: ['soon'] }] + }) + .then((result) => { + if (result.filePath == '') { + return + } + if (result.filePath.substring(result.filePath.length - 5).indexOf('.') == -1) { + result.filePath += '.soon' + } + let fs = require('fs') + o.soonType = 2 + fs.writeFileSync(result.filePath, JSON.stringify(o)) + openAs.name = result.filePath + layer.msg(language_str('saveSucc') + openAs.name) //'保存成功至' + /* + 2022-07-16 直接跳转至首页 + */ + ipcRenderer.send('open-first-page') + //saveHistory(); + //clearAll(); + }) + .catch((err) => { + console.log() + }) + layer.close(index) + }, + function (index) { + /* + 2022-07-16 直接跳转至首页 + */ + //clearAll(); + ipcRenderer.send('open-first-page') + } + ) + }) + function chagneTitle() { } + function addBackground() { + // 类型改变 + fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { + //image.set("scaleX",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) + //image.set("scaleY",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) + + image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 + image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + image.setSrc(image.toDataURL(), function (image) { + background_image1 = image + background_image = background_image1 + canvas.add(image) + canvas.renderAll() + //pre_objs1.push(JSON.stringify(objs1)); + //pre_json1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs1.push(JSON.stringify(objs1)) + recordJson1.push(canvas1.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) + }) + // 类型改变 + fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { + image.left = ($('#canvas2').width() - image.width) / 2 + image.top = ($('#canvas2').height() - image.height) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image + canvas2.add(image) + canvas2.renderAll() + //pre_objs2.push(JSON.stringify(objs2)); + //pre_json2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor"])); + + recordObjs2.push(JSON.stringify(objs2)) + recordJson2.push(canvas2.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline'])) + + let file = GetFile().get('file') + console.log(file) + if (file && file != 'empty') { + open(file) + } + }) + }) + is_bgi_add = true + }) + } + function selectObject(target, index) { + updateControls() + + $('#base_control').show() + //$("#text_x").val((target.left - background_image.left).toFixed(1)); + //$("#text_y").val((target.top - background_image.top).toFixed(1)); + //$("#text_w").val(parseFloat(target.get("scaleX") * target.get("width")).toFixed(1)); + //$("#text_h").val(parseFloat(target.get("scaleY") * target.get("height")).toFixed(1)); + //$("#text_r").val(target.get("angle").toFixed(0)); + } + function updateControls() { + $('#text_r').val(canvas.getActiveObject().get('angle').toFixed(0)) + $('#text_x').val((canvas.getActiveObject().get('left') - background_image.left).toFixed(0)) + $('#text_y').val((canvas.getActiveObject().get('top') - background_image.top).toFixed(0)) + $('#text_h').val((canvas.getActiveObject().get('scaleY') * canvas.getActiveObject().get('height')).toFixed(0)) + $('#text_w').val((canvas.getActiveObject().get('scaleX') * canvas.getActiveObject().get('width')).toFixed(0)) + if (canvas.getActiveObject().get('selectable')) { + $('#lock_img').attr('src', 'public/images/unlock20.png') //已经上锁,需要解锁 + } else { + $('#lock_img').attr('src', 'public/images/lock20.png') //没上锁,可以上锁 + } + } + function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false + } + } + for (let item of objs2) { + if (item.name == name) { + return false + } + } + return true + } + function resName(pre_name) { + //返回的是不重复的字段名 + let num = objs1.length + objs2.length - 1 + do { + num++ + } while (!checkName(pre_name + num)) //重复了就再加1 + return pre_name + num + } + function addPic(pointer) { + pre_add_image.left = pointer.x + pre_add_image.top = pointer.y + + canvas.add(pre_add_image) + + objs.push({ type: 1, black: false }) + canvas.renderAll() + updateList() + recordState() + //OpenDialog(); + //function OpenDialog() + //{ + // dialog.showOpenDialog({ + // title: "请选择图片", + // buttonLabel: "确认", + // filters: [ + // { name: 'Image File Type', extensions: ['png', 'jpg'] }, + // ] + // }).then(result => { + // if(result.canceled)return; + // //console.log(result.filePaths) + // fabric.Image.fromURL(result.filePaths[0], function(image) { + // image.left = pointer.x; + // image.top = pointer.y; + // + // canvas.add(image); + // objs.push({type: 1}); + // canvas.renderAll(); + // updateList(); + // recordState(); + // } ); + // }).catch(err => { + // console.log(err) + // }) + //} + } + function selectPic(target, index) { + //透明度 + + $('#pic_control').show() + $("#set_bg").show(); + pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + + let idx = getIndex(canvas.getActiveObject()) + // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addOutPic(pointer) { + fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + let width = canvas.getWidth() + let height = canvas.getHeight() + image.left = pointer.x + image.top = pointer.y + image.opacity = 1 + //image.on("selected",function(){//test well + // console.log(222); + //}); + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image) + objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) + canvas.renderAll() + updateList() + recordState() + }) + }) + } + function selectOutPic(target, index) { + //字段名 + $('#out_pic_control').show() + out_pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + let idx = getIndex(canvas.getActiveObject()) + $('#out_pic_field_name').val(objs[idx - 1].name) + // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + + function addText(pointer) { + let text = new fabric.IText('TEXT', { + //源:文本 + left: pointer.x, + top: pointer.y, + fill: '#000000', + textBackgroundColor: '', + fontStyle: 'normal', + fontWeight: 'normal', + underline: false, + linethrough: false, + fontFamily: 'Arial' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + canvas.add(text) + objs.push({ type: 3, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectText() { + //console.log(canvas.getActiveObject().get("fontFamily")); + $('#text_control').show() + $('#text_color').val(canvas.getActiveObject().get('fill')) + $('#text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#text_size").val(val); + $('#text_text').val(canvas.getActiveObject().get('text')) + $('#text_font_family').val(canvas.getActiveObject().get('fontFamily')) + + if (canvas.getActiveObject().get('underline') == true) { + $('#text_underline').addClass('ui-radio-acitve') + } else { + $('#text_underline').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontWeight') == 'bold') { + $('#text_bold').addClass('ui-radio-acitve') + } else { + $('#text_bold').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontStyle') == 'italic') { + $('#text_italics').addClass('ui-radio-acitve') + } else { + $('#text_italics').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('linethrough') == true) { + $('#text_linethrough').addClass('ui-radio-acitve') + } else { + $('#text_linethrough').removeClass('ui-radio-acitve') + } + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; + } + function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180/Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg + } + function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width/2,canvas.height/2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance*2, + fontSize: 30, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height/2 ? true : false + }); + // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) + // text.set({diameter: distance*2}) + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false, circleCenter: 0 }); + canvas.renderAll(); + updateList(); + recordState(); + } + function selectCircleText() { + console.log(canvas.getActiveObject().get("text")) + let idx = getIndex(canvas.getActiveObject()); + $("#circle_text_control").show(); + $("#circle_text_color").val(canvas.getActiveObject().get("fill")); + $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + circle_text_size.setValue(canvas.getActiveObject().get("fontSize") - 10); + circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")-50); + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(canvas.getActiveObject().get("text")); + $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + + } + function addOutText(pointer) { + //MERGE_TEXT_1 + let text = new fabric.Text('MERGE TEXT', { + //源:合成文本 + left: pointer.x, + top: pointer.y, + fill: '#000000', + fontStyle: 'normal', + fontWeight: 'normal', + textBackgroundColor: '', + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }) + + //text.on({ + // 'scaling': function(e) { + // var obj = this, + // w = obj.width * obj.scaleX, + // h = obj.height * obj.scaleY; + // + // obj.set({ + // 'height' : h, + // 'width' : w, + // 'scaleX' : 1, + // 'scaleY' : 1 + // }); + // } + //}); + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + + canvas.add(text) + objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) + canvas.renderAll() + updateList() + recordState() + } + function selectOutText() { + //color + //size + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $('#out_text_control').show() + $('#out_text_color').val(canvas.getActiveObject().get('fill')) + $('#out_text_text').val(canvas.getActiveObject().get('text')) + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#out_text_size").val(val); + $('#out_text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) + let idx = getIndex(canvas.getActiveObject()) + $('#out_text_field_name').val(objs[idx - 1].name) + $('#out_text_font_family').val(canvas.getActiveObject().get('fontFamily')) + $('#exceed').prop('checked', objs[idx - 1].exceed ? 'checked' : '') + $('#autoWrap').prop('checked', objs[idx - 1].autoWrap ? 'checked' : '') + $('#fixSize').prop('checked', objs[idx - 1].fixSize ? 'checked' : '') + if (canvas.getActiveObject().get('underline') == true) { + $('#out_text_underline').addClass('ui-radio-acitve') + } else { + $('#out_text_underline').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontWeight') == 'bold') { + $('#out_text_bold').addClass('ui-radio-acitve') + } else { + $('#out_text_bold').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontStyle') == 'italic') { + $('#out_text_italics').addClass('ui-radio-acitve') + } else { + $('#out_text_italics').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('linethrough') == true) { + $('#out_text_linethrough').addClass('ui-radio-acitve') + } else { + $('#out_text_linethrough').removeClass('ui-radio-acitve') + } + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 3, + width: 100, + height: 100 + }) + canvas.add(rect) + objs.push({ type: 5, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectRect() { + //color + $('#rect_control').show() + $('#rect_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) + $('#rect_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) + $('#rect_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) + $('#rect_radius').val(canvas.getActiveObject().get('rx')) + rect_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + let a = canvas.getActiveObject().getCoords() + console.log(getCoordsMinX(a)) + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 3, + radius: 100, + strokeUniform: true + }) + canvas.add(circle) + objs.push({ type: 6, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectCircle() { + //border-color + //color + $('#circle_control').show() + $('#circle_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) + $('#circle_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) + $('#circle_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) + circle_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addLine(pointer) { + let line = new fabric.Line([50, 100, 200, 100], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 3, + strokeDashArray: [0, 0], + }) + + line.setControlVisible('tl', false) + line.setControlVisible('tr', false) + line.setControlVisible('br', false) + line.setControlVisible('bl', false) + line.setControlVisible('mt', false) + line.setControlVisible('mb', false) + canvas.add(line) + objs.push({ type: 7, black: false }) + canvas.renderAll() + updateList() + recordState() + } + function selectLine() { + //color + //weight + $('#line_control').show() + + let idx = getIndex(canvas.getActiveObject()) + $('#line_color').val(canvas.getActiveObject().get('stroke')) + $('#line_dist').val(canvas.getActiveObject().get('strokeDashArray')[0]) + $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function addQrCode(pointer) { + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x + image.top = pointer.y + canvas.add(image) + objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) + canvas.renderAll() + updateList() + recordState() + }) + } + function selectQrCode() { + //字段名 + //background-color + //color + $('#qrcode_Controll').show() + let idx = getIndex(canvas.getActiveObject()) + $('#qrcode_field_name').val(objs[idx - 1].name) + $('#qrcode_color').val(objs[idx - 1].color) + $('#qrcode_val').val(objs[idx - 1].val) + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#barcode_controll').hide() + } + function addBarCode(pointer) { + JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x + image.top = pointer.y + + canvas.add(image) + objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) //show是取消文字显示 + canvas.renderAll() + updateList() + recordState() + }) + } + function selectBarCode() { + //字段名 + //background-color + //color + $('#barcode_controll').show() + let idx = getIndex(canvas.getActiveObject()) + $('#barcode_field_name').val(objs[idx - 1].name) + $('#barcode_font_family').val(objs[idx - 1].font) + let val = parseFloat(objs[idx - 1].fontSize)*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#barcode_size").val(val); + $('#barcode_val').val(objs[idx - 1].val) + $('#barcode_show').prop('checked', objs[idx - 1].show ? 'checked' : '') + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + } + function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fill: '#000000' + }) + canvas.add(text) + objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) + canvas.renderAll() + updateList() + recordState() + console.log(canvas.getObjects()) + } + function selectCounter() { + //起始 + //终止 + //增量 + $('#counter_control').show() + + let idx = getIndex(canvas.getActiveObject()) + $('#counter_start').val(objs[idx - 1].start) + $('#counter_add').val(objs[idx - 1].add) + $('#counter_color').val(canvas.getActiveObject().get('fill')) + $('#count_font_family').val(canvas.getActiveObject().get('fontFamily')) + let val = parseFloat(canvas.getActiveObject().get("fontSize"))*72/dpi + if (val%0.5!==0) val = Math.round(val*2)/2 + $("#count_size").val(val); + + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + } + function getIndex(target, _canvas = null) { + let temp_objs + if (_canvas == null) { + temp_objs = canvas.getObjects() + } else { + temp_objs = _canvas.getObjects() + } + + let i = 0 + for (let res of temp_objs) { + if (target == res) { + return i + } + i++ + } + return 0 + } + function getType(target) { + let temp_objs = canvas.getObjects() + let i = 0 + for (let res of temp_objs) { + if (target == res) { + return objs[i].type + } + i++ + } + } + + function updateList() { + let str = '' + let i = 0 + for (let item of objs) { + switch (item.type) { + case 1 /* 2022-06-14修改*/: + str = '
    IMG_" + i + '
    ' + str + break + case 2: + str = '
    " + item.name + '
    ' + str + break + case 3: + str = '
    " + 'TEXT_' + i + '
    ' + str + break + case 4: + str = '
    " + item.name + '
    ' + str + break + case 5: + str = '
    RECT_" + i + '
    ' + str + break + case 6: + str = '
    CIRCLE_" + i + '
    ' + str + break + case 7: + str = '
    LINE_" + i + '
    ' + str + break + case 8: + str = '
    QRCODE_" + i + '
    ' + str + break + case 9: + str = '
    BARCODE_" + i + '
    ' + str + break + case 10: + str = '
    COUNTER_" + i + '
    ' + str + break + case 11: + str = "
    " + "CIRCLE_TEXT_" + i + "
    " + str; + break; + } + i++ + } + $('.obj_list').html(str) + } + function selectObj(i, notSelect = false) { + //for(let item in $(".obj_list").children()){ + // if(_i == i){ + // item.css("background-color","#6F7A84"); + // }else{ + // item.css("background-color",""); + // } + // _i++; + //} + $('.obj_list div').each(function (index) { + //if(index == i){ + if ($(this).children('img').attr('id') == 'obj' + i) { + $(this).css('background-color', '#6F7A84') + canvas.setActiveObject(canvas.getObjects()[i + 1]) + canvas.renderAll() + } else { + $(this).css('background-color', '') + } + }) + if (!notSelect) { + handleObjectSelected() + } + } + function recordState() { + //pre_objs.push(JSON.stringify(objs)); + //pre_json.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + //next_json = []; + //next_objs = []; + //recordObjs.push(JSON.stringify(objs)); + //recordJson.push(canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])); + ++step.val + for (let i = step.val; i < recordObjs.length; i++) { + recordObjs.pop() + recordJson.pop() + } + //recordObjs.splice(step.val, 1, JSON.stringify(objs)) + //recordJson.splice(step.val, 1, canvas.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"])) + recordObjs.push(JSON.stringify(objs)) + j = canvas.toJSON(['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline']) + j.objects[0].hoverCursor = 'default' + recordJson.push(j) + console.log(recordJson) + } + function getCoordsMinX(acoords) { + x = acoords[0].x + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x + } + function getCoordsMaxX(acoords) { + x = acoords[0].x + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x + } + function getCoordsMinY(acoords) { + y = acoords[0].y + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y + } + function getCoordsMaxY(acoords) { + y = acoords[0].y + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y + } + + $('#text_y').bind('input propertychange', function () { + if ($('#text_y').val() == '') { + $('#text_y').val(0) + } + let val = background_image.top + if ($('#text_y').val() != '') { + val = background_image.top + parseInt($('#text_y').val()) + } + canvas.getActiveObject().set('top', val) + canvas.renderAll() + recordState() + }) + $('#text_x').bind('input propertychange', function () { + if ($('#text_x').val() == '') { + $('#text_x').val(0) + } + let val = background_image.left + if ($('#text_x').val() != '') { + val = background_image.left + parseInt($('#text_x').val()) + } + canvas.getActiveObject().set('left', val) + canvas.renderAll() + recordState() + }) + $('#text_w').bind('input propertychange', function () { + let val = '0' + if ($('#text_w').val() != '') { + val = $('#text_w').val() + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type + if (type == 3 || type == 4) { + canvas.getActiveObject().set('width', parseInt(val)) + } else { + canvas.getActiveObject().set('scaleX', parseInt(val) / canvas.getActiveObject().get('width')) + } + + canvas.renderAll() + recordState() + }) + $('#text_h').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + if (objs[idx - 1].type == 7) { + if ($('#text_h').val() == '') { + $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#text_h').val())) + canvas.renderAll() + return + } + let val = '0' + if ($('#text_h').val() != '') { + val = $('#text_h').val() + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type + if (type == 3 || type == 4) { + canvas.getActiveObject().set('height', parseInt(val)) + } else { + canvas.getActiveObject().set('scaleY', parseInt(val) / canvas.getActiveObject().get('height')) + } + + canvas.renderAll() + recordState() + }) + $('#text_r').bind('input propertychange', function () { + if ($('#text_r').val() == '') { + $('#text_r').val('0') + } + $('#text_r').val(parseInt($('#text_r').val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($('#text_r').val())) + canvas.renderAll() + recordState() + }) + $('#out_pic_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#out_pic_field_name').val() + updateList() + recordState() + }) + + $('#text_black').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].black = true + } else { + objs[idx - 1].black = false + } + recordState() + }) + // $("#out_pic_black").click(function () { + // let idx = getIndex(canvas.getActiveObject()); + // if ($(this).prop("checked")) { + // objs[idx-1].black = true; + // } else { + // objs[idx-1].black = false; + // } + // recordState(); + // }); + $('#out_text_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#out_text_field_name').val() + updateList() + recordState() + }) + $('#qrcode_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#qrcode_field_name').val() + updateList() + recordState() + }) + $('#qrcode_color').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0, foreground: $(this).val() }) + let idx = getIndex(item) + objs[idx - 1].color = $(this).val() + item.setSrc(qr, function (img) { + canvas.renderAll() + recordState() + }) + }) + $("#qrcode_val").bind('change', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $('#barcode_color').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].color = $(this).val() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + }) + $('#barcode_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#barcode_field_name').val() + updateList() + recordState() + }) + $('#text_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#text_color').val()) + canvas.renderAll() + recordState() + }) + $('#text_back_color').bind('input propertychange', function () { + canvas.getActiveObject().set('textBackgroundColor', $('#text_back_color').val()) + canvas.renderAll() + recordState() + }) + $('#out_text_back_color').bind('input propertychange', function () { + canvas.getActiveObject().set('textBackgroundColor', $('#out_text_back_color').val()) + + canvas.renderAll() + recordState() + }) + $("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + recordState(); + }) + $("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + recordState(); + }) + $("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll(); + updateList(); + recordState(); + }) + $("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + recordState(); + }); + $('#out_text_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#out_text_color').val()) + canvas.renderAll() + recordState() + }) + $("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $('#out_text_text').bind('input propertychange', function () { + let val = $("#out_text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll() + updateList() + recordState() + }) + $('#rect_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#rect_color').val()) + + canvas.renderAll() + recordState() + }) + $('#rect_stroke').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#rect_stroke').val()) + + canvas.renderAll() + recordState() + }) + $('#rect_stroke_width').bind('input propertychange', function () { + if ($('#rect_stroke_width').val() == '') { + $('#rect_stroke_width').val('0') + } else { + $('#rect_stroke_width').val(parseInt($('#rect_stroke_width').val())) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#rect_stroke_width').val())) + canvas.renderAll() + recordState() + }) + $('#rect_radius').bind('input propertychange', function () { + if ($('#rect_radius').val() == '') { + $('#rect_radius').val('0') + } else { + $('#rect_radius').val(parseInt($('#rect_radius').val())) + } + canvas.getActiveObject().set('rx', parseInt($('#rect_radius').val())) + canvas.getActiveObject().set('ry', parseInt($('#rect_radius').val())) + canvas.renderAll() + recordState() + }) + $('#circle_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#circle_color').val()) + + canvas.renderAll() + recordState() + }) + $('#circle_stroke').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#circle_stroke').val()) + + canvas.renderAll() + recordState() + }) + $('#circle_stroke_width').bind('input propertychange', function () { + if ($('#circle_stroke_width').val() == '') { + $('#circle_stroke_width').val('0') + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#circle_stroke_width').val())) + canvas.renderAll() + recordState() + }) + $('#counter_start').bind('input propertychange', function () { + canvas.getActiveObject().set('text', $('#counter_start').val()) + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].start = $(this).val() + canvas.renderAll() + recordState() + }) + $('#counter_add').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].add = $(this).val() + canvas.renderAll() + recordState() + }) + $('#counter_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#counter_color').val()) + canvas.renderAll() + recordState() + }) + $("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val*dpi/72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); + }) + $('#line_color').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#line_color').val()) + canvas.renderAll() + recordState() + }) + $('#line_dist').bind('input propertychange', function () { + if ($('#line_dist').val() == '') { + $('#line_dist').val(0) + } + canvas.getActiveObject().set('strokeDashArray', [parseInt($('#line_dist').val()), parseInt($('#line_dist').val())]) + canvas.renderAll() + recordState() + }) + $('#text_text').bind('input propertychange', function () { + let val = $("#text_text").val().trim() + canvas.getActiveObject().set("text", ` ${val} `); + canvas.renderAll() + updateList() + recordState() + }) + $('#barcode_val').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].val = $(this).val() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + }) + $("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val() + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + val = 0 + } else { + objs[idx - 1].show = false; + } + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val*dpi/72, font: objs[idx - 1].font }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }); + $("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val%0.5!==0) { + val = Math.round(val*2)/2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val*dpi/72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); + }) + $('#count_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() + }) + $('#text_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() + }) + + $('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); + }); + + $('#language_select').on('change', function () { + //语言选择 + langua_ge($(this).find('option:selected').val()) + s_lan = $(this).find('option:selected').val() + localStorage.setItem('lang', s_lan) + }) + $('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].font = $(this).find('option:selected').text() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + //recordState(); + }) + $('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() + }) + $('.obj_list').on('click', 'div', function () { + let idx = objs.length - 1 - $(this).index() + selectObj(idx) + }) + $('#exceed').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].exceed = true + } else { + objs[idx - 1].exceed = false + } + recordState() + }) + $('#autoWrap').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].autoWrap = true + } else { + objs[idx - 1].autoWrap = false + } + recordState() + }) + $('#fixSize').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].fixSize = true + } else { + objs[idx - 1].fixSize = false + } + recordState() + }) + $('#delete').click(function () { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + updateList() + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + } + // let actObjs = canvas.getActiveObjects(); + // let allObjs = canvas.getObjects(); + // let flag = 0; + // for( let item of actObjs){ + // if ( item != allObjs[0]){ + // let i = getIndex(item);//0是背景,所以从1开始,但objs不包含背景图片,故要-1 + // objs.splice(i-1,1); + // canvas.remove(item); + // flag = 1; + // updateList(); + // } + // } + // selectObj(-1); + // if(flag == 1){ + // recordState(); + // } + }) + + //在关于我们获取版本号。 + // 注意:function 前面加了 async 关键字 + $("#about").click(async function () { + + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + console.error("获取版本号失败", e); + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
    ' + + '
    ' + + '
    Soon Design
    ' + + // 【修改点】这里使用了刚才获取到的 version 变量 + '
    ' + language_str("vers") + ' ' + version + '
    ' + + '
    ' + language_str("copyright") + '
    ' + + '
    ' + }); + }) + $("#help").click(() => { + ipcRenderer.send('open-help-file'); + }); + function handleObjectSelected() { + let select_obj = canvas.getActiveObject() + if (!select_obj) { + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //comfirm + return + } + let i = getIndex(select_obj) + if (i != 0) { + //不是背景 + //console.log(objs); + --i + switch (objs[i].type) { + case 1: + //console.log("图片"); + $('#component_type').text(language_str('img')) //"图片" + selectObject(select_obj, i) + selectPic(select_obj, i) + break + case 2: + //console.log("预留图片"); + + $('#component_type').text(language_str('simg')) //"合成图片" + selectObject(select_obj, i) + selectOutPic(select_obj, i) + break + case 3: + $('#component_type').text(language_str('text')) //"文字" + //console.log("文字"); + selectObject(select_obj, i) + selectText(select_obj, i) + break + case 4: + //console.log("标签"); + + $('#component_type').text(language_str('stext')) //"合成文字" + selectObject(select_obj, i) + selectOutText(select_obj, i) + break + case 5: + //console.log("矩形"); + + $('#component_type').text(language_str('rect')) //"矩形" + selectObject(select_obj, i) + selectRect(select_obj, i) + break + case 6: + //console.log("圆形"); + + $('#component_type').text(language_str('circ')) //"圆形" + selectObject(select_obj, i) + selectCircle(select_obj, i) + break + case 7: + //console.log("直线"); + + $('#component_type').text(language_str('line')) //"直线" + selectObject(select_obj, i) + selectLine(select_obj, i) + break + case 8: + //console.log("二维码"); + + $('#component_type').text(language_str('qrc')) //"二维码" + selectObject(select_obj, i) + selectQrCode(select_obj, i) + break + case 9: + //console.log("条形码"); + + $('#component_type').text(language_str('barc')) //"条形码" + selectObject(select_obj, i) + selectBarCode(select_obj, i) + break + case 10: + //console.log("计数器"); + + $('#component_type').text(language_str('counter')) //"计数器" + selectObject(select_obj, i) + selectCounter(select_obj, i) + break + case 11: + + $("#component_type").text(language_str("ctext"));//"圆形文字" + //console.log("圆形文字"); + selectObject(select_obj, i); + selectCircleText(select_obj, i); + break; + } + } else { + //背景,收起对象属性 + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) + } + } + + canvas1.on('mouse:move', (e) => { + let width = canvas.getWidth() + let height = canvas.getHeight() + $('#posText').text( + 'X: ' + (e.pointer.x - (width / 2 - (width / 2 - background_image.left) * zoom)).toFixed(0) + ' Y: ' + (e.pointer.y - (height / 2 - (height / 2 - background_image.top) * zoom)).toFixed(0) + ) + }) + canvas1.on("object:moving", function (e) { + let obj = canvas1.getActiveObject() + let idx = getIndex(e.target, canvas1); + // 添加边界检查,避免访问 undefined + if (idx > 0 && idx <= objs1.length && objs1[idx - 1] && objs1[idx - 1].type && objs1[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { + x: obj.left, + y: obj.top + } + let distance = getDisdance(pointer.x, pointer.y, canvas1.width/2, canvas1.height/2) + let angleDeg = getDeg(pointer) + let flipped = pointer.y > canvas1.height/2 ? true : false + circle_text_diameter.setValue(distance*2-50); + obj.set("diameter", distance*2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); + }); + // 监听键盘事件 + + // 复制选中对象 + function copySelection() { + const activeObjects = canvas.getActiveObjects(); + + if (!activeObjects || activeObjects.length === 0) { + console.log("未选中任何对象"); + layer.msg("未选中任何对象") + return; + } + + // 存储原始对象用于克隆 + clipboardData.objects = activeObjects; + console.log(activeObjects) + console.log(`已复制 ${activeObjects.length} 个对象`); + } + + // 粘贴对象 + function pasteFromClipboard() { + if (!clipboardData.objects || clipboardData.objects.length === 0) { + console.log("剪贴板为空"); + layer.msg("剪贴板为空") + return; + } + + // 克隆所有对象 + fabric.util.enlivenObjects( + clipboardData.objects.map(obj => obj.toObject()), + clonedObjects => { + // 设置新位置(添加偏移) + clonedObjects.forEach((clone, index) => { + let idx = getIndex(clipboardData.objects[index]) + console.log(clone) + clone.set({ + left: clone.left + clipboardData.offset, + top: clone.top + clipboardData.offset, + evented: true + }); + // 计算整体偏移 + const groupOffset = clone.group; + if (groupOffset) { + clone.set({ + left: groupOffset.left + clipboard.offset, + top: groupOffset.top + clipboard.offset + }); + } + + // 添加到画布 + canvas.add(clone); + let tobj = {...objs[idx-1]} + if (tobj.name) tobj.name = tobj.name + objs.length + objs.push(tobj); + canvas.renderAll(); + updateList(); + recordState(); + }); + + // 选中粘贴的对象 + canvas.discardActiveObject(); + const sel = new fabric.ActiveSelection(clonedObjects, { + canvas: canvas + }); + canvas.setActiveObject(sel); + canvas.requestRenderAll(); + selectObj(objs.length - 1); + // 增加下次偏移 + clipboardData.offset += 10; + } + ); + } + canvas1.on('object:scaling', function (event) { + //if(objs[getIndex(event.target) - 1].type == 5 || objs[getIndex(event.target) - 1].type == 6){ + // //canvas.getActiveObject().set("width", canvas.getActiveObject().get("width") * canvas.getActiveObject().get("scaleX")); + // //canvas.getActiveObject().set("height", canvas.getActiveObject().get("height") * canvas.getActiveObject().get("scaleY")); + // //canvas.getActiveObject().set("scaleX", 1); + // //canvas.getActiveObject().set("scaleY", 1); + // //console.log(canvas.getActiveObject().get("scaleX")); + // //canvas1.discardActiveObject(); + // //canvas1.setActiveObject(event.target); + // //canvas1.renderAll(); + // //updateControls(); + //} + }) + canvas1.on('object:rotating', function (e) { + updateControls() + }) + canvas1.on('object:rotated', function (e) { + console.log('object:rotated') + recordState() + }) + canvas1.on('object:added', function (e) { + //console.log("object:added"); + //recordState(); + ////console.log(objs); + }) + canvas1.on('mouse:down', (e) => { + //单选 + let select_obj = canvas.getActiveObject() + console.log(select_obj) + let i = getIndex(select_obj) + if (i != 0) { + //不是背景 + //console.log(objs); + --i + switch (objs[i].type) { + case 1: + //console.log("图片"); + $('#component_type').text(language_str('img')) //"图片" + selectObject(select_obj, i) + selectPic(select_obj, i) + break + case 2: + //console.log("预留图片"); + + $('#component_type').text(language_str('simg')) //"合成图片" + selectObject(select_obj, i) + selectOutPic(select_obj, i) + break + case 3: + $('#component_type').text(language_str('text')) //"文字" + //console.log("文字"); + selectObject(select_obj, i) + selectText(select_obj, i) + break + case 4: + //console.log("标签"); + + $('#component_type').text(language_str('stext')) //"合成文字" + selectObject(select_obj, i) + selectOutText(select_obj, i) + break + case 5: + //console.log("矩形"); + + $('#component_type').text(language_str('rect')) //"矩形" + selectObject(select_obj, i) + selectRect(select_obj, i) + break + case 6: + //console.log("圆形"); + + $('#component_type').text(language_str('circ')) //"圆形" + selectObject(select_obj, i) + selectCircle(select_obj, i) + break + case 7: + //console.log("直线"); + + $('#component_type').text(language_str('line')) //"直线" + selectObject(select_obj, i) + selectLine(select_obj, i) + break + case 8: + //console.log("二维码"); + + $('#component_type').text(language_str('qrc')) //"二维码" + selectObject(select_obj, i) + selectQrCode(select_obj, i) + break + case 9: + //console.log("条形码"); + + $('#component_type').text(language_str('barc')) //"条形码" + selectObject(select_obj, i) + selectBarCode(select_obj, i) + break + case 10: + //console.log("计数器"); + + $('#component_type').text(language_str('counter')) //"计数器" + selectObject(select_obj, i) + selectCounter(select_obj, i) + break + case 11: + + $("#component_type").text(language_str("ctext"));//"圆形文字" + //console.log("圆形文字"); + selectObject(select_obj, i); + selectCircleText(select_obj, i); + break; + } + selectObj(i, true) //如果是点击画布中,则相应列表 + } else { + //背景,收起对象属性 + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + selectObj(-1) + $('#component_type').text(language_str('bg')) + } + }) + canvas1.on('text:editing:entered', function () { + _textEdit = true + }) + canvas1.on('text:editing:exited', function () { + _textEdit = false + }) + canvas1.on('object:modified', function (event) { + if (canvas1.getActiveObjects().length > 1) { + return + } + if (objs[getIndex(event.target) - 1].type == 3) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#text_size').val(event.target.fontSize) + canvas1.renderAll() + updateControls() + } else if (objs[getIndex(event.target) - 1].type == 4) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#out_text_size').val(event.target.fontSize) + canvas1.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 10) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#count_size').val(event.target.fontSize) + canvas1.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 5) { + canvas1.getActiveObject().set('width', canvas1.getActiveObject().get('width') * canvas1.getActiveObject().get('scaleX')) + canvas1.getActiveObject().set('height', canvas1.getActiveObject().get('height') * canvas1.getActiveObject().get('scaleY')) + canvas1.getActiveObject().set('scaleX', 1) + canvas1.getActiveObject().set('scaleY', 1) + updateControls() + } + recordState() + }) + canvas1.on('mouse:up', (e) => { + //HERE + switch (addState) { + case 1: + addPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addPic').css('background-color', '') + break + case 2: + addOutPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addOutPic').css('background-color', '') + break + case 3: + addText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addText').css('background-color', '') + break + case 4: + addOutText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addOutText').css('background-color', '') + break + case 5: + addRect(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addRect').css('background-color', '') + break + case 6: + addCircle(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addCircle').css('background-color', '') + break + case 7: + addLine(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addLine').css('background-color', '') + break + case 8: + addQrCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addQrCode').css('background-color', '') + break + case 9: + addBarCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas1.renderAll() + $('#addBarCode').css('background-color', '') + break + case 10: + addCounter(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + $('#addCounter').css('background-color', '') + break + case 11: + addCircleText(e.pointer); + addState = 0; + background_image.hoverCursor = "default"; + canvas1.renderAll(); + $("#addCircleText").css("background-color", ""); + break; + } + }) + canvas1.on('text:changed', (e) => { + $('#text_text').val(e.target.get('text')) + }) + canvas1.on('after:render', function () { + if (is_bgi_add) { + var image = document.getElementById('source_front') + ctx1.drawImage(image, (canvas.width - $('#source_front').width()) / 2, (canvas.height - $('#source_front').height()) / 2, $('#source_front').width(), $('#source_front').height()) + + /* + 实际905 * 534 + 1052 * + */ + } + }) + canvas2.on('text:editing:entered', function () { + _textEdit = true + }) + canvas2.on('text:editing:exited', function () { + _textEdit = false + }) + canvas2.on('after:render', function () { + if (is_bgi_add) { + var image = document.getElementById('source_back') + ctx2.drawImage(image, (canvas.width - $('#source_back').width()) / 2, (canvas1.height - $('#source_back').height()) / 2, $('#source_back').width(), $('#source_back').height()) + } + }) + canvas2.on('mouse:move', (e) => { + $('#posText').text('X: ' + (e.pointer.x - background_image.left).toFixed(0) + ' Y: ' + (e.pointer.y - background_image.top).toFixed(0)) + }) + canvas2.on("object:moving", function (e) { + let obj = canvas2.getActiveObject() + let idx = getIndex(e.target, canvas2); + // 添加边界检查,避免访问 undefined + if (idx > 0 && idx <= objs2.length && objs2[idx - 1] && objs2[idx - 1].type && objs2[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) { + let pointer = { + x: obj.left, + y: obj.top + } + let distance = getDisdance(pointer.x, pointer.y, canvas2.width/2, canvas2.height/2) + // 计算角度时需要使用 canvas2 + let centerX = canvas2.width / 2; + let centerY = canvas2.height / 2; + let mouseX = pointer.x; + let mouseY = pointer.y; + let dx = mouseX - centerX; + let dy = mouseY - centerY; + let angleRad = Math.atan2(dy, dx); + let angleDeg = angleRad * (180/Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90; + let flipped = pointer.y > canvas2.height/2 ? true : false + circle_text_diameter.setValue(distance*2-50); + obj.set("diameter", distance*2); + obj.set("angle", angleDeg); + obj.set("flipped", flipped); + } + updateControls(); + }); + canvas2.on('object:scaling', function (e) { + updateControls() + }) + canvas2.on('object:rotating', function (e) { + updateControls() + }) + canvas2.on('object:rotated', function (e) { + console.log('object:rotated') + recordState() + }) + canvas2.on('object:added', function (e) { + //console.log("object:added"); + //recordState(); + ////console.log(objs); + }) + canvas2.on('mouse:down', (e) => { + //单选 + let select_obj = canvas.getActiveObject() + let i = getIndex(select_obj) + if (i != 0) { + //不是背景 + //console.log(objs); + --i + switch (objs[i].type) { + case 1: + //console.log("图片"); + $('#component_type').text(language_str('img')) //"图片" + selectObject(select_obj, i) + selectPic(select_obj, i) + break + case 2: + //console.log("预留图片"); + + $('#component_type').text(language_str('simg')) //"合成图片" + selectObject(select_obj, i) + selectOutPic(select_obj, i) + break + case 3: + $('#component_type').text(language_str('text')) //"文字" + //console.log("文字"); + selectObject(select_obj, i) + selectText(select_obj, i) + break + case 4: + //console.log("标签"); + + $('#component_type').text(language_str('stext')) //"合成文字" + selectObject(select_obj, i) + selectOutText(select_obj, i) + break + case 5: + //console.log("矩形"); + + $('#component_type').text(language_str('rect')) //"矩形" + selectObject(select_obj, i) + selectRect(select_obj, i) + break + case 6: + //console.log("圆形"); + + $('#component_type').text(language_str('circ')) //"圆形" + selectObject(select_obj, i) + selectCircle(select_obj, i) + break + case 7: + //console.log("直线"); + + $('#component_type').text(language_str('line')) //"直线" + selectObject(select_obj, i) + selectLine(select_obj, i) + break + case 8: + //console.log("二维码"); + + $('#component_type').text(language_str('qrc')) //"二维码" + selectObject(select_obj, i) + selectQrCode(select_obj, i) + break + case 9: + //console.log("条形码"); + + $('#component_type').text(language_str('barc')) //"条形码" + selectObject(select_obj, i) + selectBarCode(select_obj, i) + break + case 10: + //console.log("计数器"); + + $('#component_type').text(language_str('counter')) //"计数器" + selectObject(select_obj, i) + selectCounter(select_obj, i) + break + case 11: + + $("#component_type").text(language_str("ctext"));//"圆形文字" + //console.log("圆形文字"); + selectObject(select_obj, i); + selectCircleText(select_obj, i); + break; + } + selectObj(i, true) //如果是点击画布中,则相应列表 + } else { + //背景,收起对象属性 + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#counter_control').hide() + selectObj(-1) + $('#component_type').text(language_str('bg')) + } + }) + canvas2.on('object:modified', function (event) { + if (canvas.getActiveObjects().length > 1) { + return + } + if (objs[getIndex(event.target) - 1].type == 3) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#text_size').val(event.target.fontSize) + canvas2.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 4) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#out_text_size').val(event.target.fontSize) + canvas2.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 10) { + event.target.fontSize *= event.target.scaleX + event.target.fontSize = event.target.fontSize.toFixed(0) + event.target.scaleX = 1 + event.target.scaleY = 1 + event.target._clearCache() + // $('#count_size').val(event.target.fontSize) + canvas2.renderAll() + } else if (objs[getIndex(event.target) - 1].type == 5) { + canvas2.getActiveObject().set('width', canvas2.getActiveObject().get('width') * canvas2.getActiveObject().get('scaleX')) + canvas2.getActiveObject().set('height', canvas2.getActiveObject().get('height') * canvas2.getActiveObject().get('scaleY')) + canvas2.getActiveObject().set('scaleX', 1) + canvas2.getActiveObject().set('scaleY', 1) + updateControls() + } + console.log('object:modified') + recordState() + }) + canvas2.on('mouse:up', (e) => { + //HERE + switch (addState) { + case 1: + addPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addPic').css('background-color', '') + break + case 2: + addOutPic(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addOutPic').css('background-color', '') + break + case 3: + addText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addText').css('background-color', '') + break + case 4: + addOutText(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addOutText').css('background-color', '') + break + case 5: + addRect(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addRect').css('background-color', '') + break + case 6: + addCircle(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addCircle').css('background-color', '') + break + case 7: + addLine(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addLine').css('background-color', '') + break + case 8: + addQrCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addQrCode').css('background-color', '') + break + case 9: + addBarCode(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addBarCode').css('background-color', '') + break + case 10: + addCounter(e.pointer) + addState = 0 + background_image.hoverCursor = 'default' + canvas2.renderAll() + $('#addCounter').css('background-color', '') + break + case 11: + addCircleText(e.pointer); + addState = 0; + background_image.hoverCursor = "default"; + canvas2.renderAll(); + $("#addCircleText").css("background-color", ""); + break; + } + }) + var circle_text_size = slider.render({ + elem: '#circle_text_size', + min: 10, + max: 120 + , change: function (value) { + canvas.getActiveObject().set("fontSize", value); + canvas.renderAll(); + } + }); + var circle_text_diameter = slider.render({ + elem: '#circle_text_diameter', + min: 50, + max: 1100 + , change: function (value) { + canvas.getActiveObject().set("diameter", value); + canvas.renderAll(); + } + }); + var out_pic_op = slider.render({ + elem: '#out_pic_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + var pic_op = slider.render({ + elem: '#pic_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + var rect_op = slider.render({ + elem: '#rect_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + var circle_op = slider.render({ + elem: '#circle_op', + change: function (value) { + console.log(value) + console.log(canvas.getActiveObject()) + canvas.getActiveObject().set('opacity', (100 - value) / 100) + canvas.renderAll() + } + }) + + // 标尺绘制函数 + function initRulers() { + const dpi = 300; + const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI) + const rulerHeight = 20; + const rulerWidth = 20; + + // 获取实际画布的像素尺寸(px) + // design2.html 的实际画布尺寸是 1012px x 648px + const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px) + const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px) + + // 获取显示区域的尺寸(包括 margin) + const canvasDiv = $('#canvas-div'); + const canvasWrapper = $('.canvas-wrapper'); + // 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度 + let canvasWidth = canvasWrapper.width(); + let canvasHeight = canvasWrapper.height(); + if (!canvasWidth || canvasWidth === 0) { + const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20; + canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!canvasHeight || canvasHeight === 0) { + const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20; + canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + + // 设置标尺尺寸(使用显示区域的尺寸) + $('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight); + $('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight); + $('.ruler-horizontal').css('width', canvasWidth + 'px'); + $('.ruler-vertical').css('height', canvasHeight + 'px'); + + // 绘制水平标尺 + function drawHorizontalRuler() { + const ctx = document.getElementById('ruler-h').getContext('2d'); + ctx.clearRect(0, 0, canvasWidth, rulerHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, canvasWidth, rulerHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvasWidth, rulerHeight); + ctx.beginPath(); + ctx.moveTo(0, rulerHeight - 1); + ctx.lineTo(canvasWidth, rulerHeight - 1); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + // 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm) + // 显示区域宽度 = actualCanvasWidth * zoom (px) + // 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const startX = 0; + const endX = canvasWidth; + + // 绘制主要刻度(每10mm) + for (let mm = 0; mm * pxPerMm <= endX; mm += 10) { + const x = startX + mm * pxPerMm; + if (x > endX) break; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + + // 数字 + ctx.fillText(mm + 'mm', x + 2, 2); + } + + // 绘制次要刻度(每1mm) + for (let mm = 0; mm * pxPerMm <= endX; mm += 1) { + const x = startX + mm * pxPerMm; + if (x > endX) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(x, rulerHeight - 8); + ctx.lineTo(x, rulerHeight); + ctx.stroke(); + } + } + + // 绘制垂直标尺 + function drawVerticalRuler() { + const ctx = document.getElementById('ruler-v').getContext('2d'); + ctx.clearRect(0, 0, rulerWidth, canvasHeight); + + // 背景 + ctx.fillStyle = '#2C2C2C'; + ctx.fillRect(0, 0, rulerWidth, canvasHeight); + + // 边框 + ctx.strokeStyle = '#1E1E1E'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, rulerWidth, canvasHeight); + ctx.beginPath(); + ctx.moveTo(rulerWidth - 1, 0); + ctx.lineTo(rulerWidth - 1, canvasHeight); + ctx.stroke(); + + // 刻度线 + ctx.strokeStyle = '#888888'; + ctx.fillStyle = '#CCCCCC'; + ctx.font = '10px Arial'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算 + const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数 + const startY = 0; + const endY = canvasHeight; + + // 绘制主要刻度(每10mm) + for (let mm = 0; mm * pxPerMm <= endY; mm += 10) { + const y = startY + mm * pxPerMm; + if (y > endY) break; + + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + + // 数字(旋转90度) + ctx.save(); + ctx.translate(2, y + 8); + ctx.rotate(-Math.PI / 2); + ctx.fillText(mm + 'mm', 0, 0); + ctx.restore(); + } + + // 绘制次要刻度(每1mm) + for (let mm = 0; mm * pxPerMm <= endY; mm += 1) { + const y = startY + mm * pxPerMm; + if (y > endY) break; + if (mm % 10 === 0) continue; // 跳过主要刻度 + + ctx.beginPath(); + ctx.moveTo(rulerWidth - 8, y); + ctx.lineTo(rulerWidth, y); + ctx.stroke(); + } + } + + // 初始绘制 + drawHorizontalRuler(); + drawVerticalRuler(); + + // 监听窗口大小变化 + $(window).on('resize', function() { + setTimeout(function() { + const canvasWrapper = $('.canvas-wrapper'); + let newWidth = canvasWrapper.width(); + let newHeight = canvasWrapper.height(); + if (!newWidth || newWidth === 0) { + const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20; + newWidth = canvas1Width + 20; // 加上 margin-left: 20px + } + if (!newHeight || newHeight === 0) { + const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20; + newHeight = canvas1Height + 20; // 加上 margin-top: 20px + } + if (newWidth !== canvasWidth || newHeight !== canvasHeight) { + canvasWidth = newWidth; + canvasHeight = newHeight; + $('#ruler-h').attr('width', newWidth); + $('#ruler-v').attr('height', newHeight); + $('.ruler-horizontal').css('width', newWidth + 'px'); + $('.ruler-vertical').css('height', newHeight + 'px'); + drawHorizontalRuler(); + drawVerticalRuler(); + } + }, 100); + }); + + // 监听画布缩放变化 + canvas1.on('after:render', function() { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + canvas2.on('after:render', function() { + drawHorizontalRuler(); + drawVerticalRuler(); + }); + } + }) diff --git a/lib/design2.js b/frontend-web/js/design2.js similarity index 97% rename from lib/design2.js rename to frontend-web/js/design2.js index 048bc30..eda1410 100644 --- a/lib/design2.js +++ b/frontend-web/js/design2.js @@ -41,6 +41,7 @@ if (ipcRenderer) { var $ = layui.$; var layer = layui.layer; + layer.config({ skin: 'soon-layer' }); var slider = layui.slider; var form = layui.form; var colorpicker = layui.colorpicker; @@ -401,7 +402,8 @@ if (ipcRenderer) { if (val == "") { $("title").html('Soon Design'); } else { - $("title").html('Soon Design - ' + val); + var titleName = (typeof window.soonFormatOpenTitle === 'function') ? window.soonFormatOpenTitle(val) : val; + $("title").html('Soon Design - ' + titleName); } this._name = val; }, @@ -497,9 +499,9 @@ if (ipcRenderer) { s.onload = s.onerror = function () { if (typeof next === 'function') next(); }; document.body.appendChild(s); } - loadScript('./lib/design2/output.js', function () { - loadScript('./lib/design2/core.js', function () { - loadScript('./lib/design2/ui.js', function () {}); + loadScript('../js/design2/output.js', function () { + loadScript('../js/design2/core.js', function () { + loadScript('../js/design2/ui.js', function () {}); }); }); return; @@ -507,7 +509,7 @@ if (ipcRenderer) { const loadModule = (moduleName) => { if (!path || !fs) return; - const jsPath = path.join(appPath, 'lib', 'design2', moduleName + '.js'); + const jsPath = path.join(appPath, 'js', 'design2', moduleName + '.js'); if (fs.existsSync(jsPath)) { try { eval(fs.readFileSync(jsPath, 'utf8')); diff --git a/lib/design2/core.js b/frontend-web/js/design2/core.js similarity index 98% rename from lib/design2/core.js rename to frontend-web/js/design2/core.js index dfaaaf1..aab9f56 100644 --- a/lib/design2/core.js +++ b/frontend-web/js/design2/core.js @@ -524,9 +524,9 @@ window.updateControls = function updateControls() { $("#text_w").val((currentCanvas.getActiveObject().get("scaleX") * currentCanvas.getActiveObject().get("width")).toFixed(0)); // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) if (currentCanvas.getActiveObject().get("lockMovementX")) { - $("#lock_img").attr("src", "public/images/lock20.png")//已锁定 + $("#lock_img").attr("src", soonAsset("lock20.png"))//已锁定 } else { - $("#lock_img").attr("src", "public/images/unlock20.png")//未锁定 + $("#lock_img").attr("src", soonAsset("unlock20.png"))//未锁定 } }; // 向后兼容 @@ -1302,13 +1302,12 @@ window.updateList = function updateList() { // 锁定状态图标 const lockIcon = isLocked ? - '' : - ''; + `` : + ``; - // 组装 HTML str = `
    - + ${nameText} ${lockIcon}
    @@ -1593,7 +1592,7 @@ function getCoordsMaxY(acoords) { function addBackground() { // 类型改变 - fabric.Image.fromURL('./public/images/bg_front_2.png', function (image) { + fabric.Image.fromURL(soonAsset('bg_front_2.png'), function (image) { image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 @@ -1613,7 +1612,7 @@ function addBackground() { recordJson1.push(canvas1.toJSON(props)) }) // 类型改变 - fabric.Image.fromURL('./public/images/bg_back_2.png', function (image) { + fabric.Image.fromURL(soonAsset('bg_back_2.png'), function (image) { image.left = ($('#canvas2').width() - image.width) / 2 image.top = ($('#canvas2').height() - image.height) / 2 @@ -1641,6 +1640,15 @@ function addBackground() { } catch (e) {} } if (file && file != 'empty') { + if (file.indexOf('soondesign_file:') === 0 && typeof sessionStorage !== 'undefined') { + try { + var metaRaw = sessionStorage.getItem('soondesign_open_meta'); + if (metaRaw) { + window._soonFileMeta = JSON.parse(metaRaw); + sessionStorage.removeItem('soondesign_open_meta'); + } + } catch (e) {} + } var p = window.openFile(file); if (p && p.then) p.then(function() {}, function() {}); } @@ -1663,7 +1671,7 @@ function addPic(pointer) { } function addOutPic(pointer) { - fabric.Image.fromURL('./public/images/outpic_extra.png', function (image) { + fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) { let width = canvas.getWidth() let height = canvas.getHeight() image.left = pointer.x diff --git a/frontend-web/js/design2/output.js b/frontend-web/js/design2/output.js new file mode 100644 index 0000000..16958c0 --- /dev/null +++ b/frontend-web/js/design2/output.js @@ -0,0 +1,1527 @@ +// 将 display_func 附加到 window 对象,确保全局可访问 +window.display_func = function display_func(img1, img2, img3) { + if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return; + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $('#circle_text_control').hide() + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + + // 保存所有对象的 selectable 和 evented 状态,确保预览后能恢复 + const objStates1 = []; + const objStates2 = []; + canvas1.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine) { + objStates1[index] = { + selectable: obj.selectable, + evented: obj.evented + }; + } + }); + canvas2.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine) { + objStates2[index] = { + selectable: obj.selectable, + evented: obj.evented + }; + } + }); + + // 过滤掉辅助线 + let _objs1 = canvas1.getObjects().filter(obj => !obj.isGuideLine) + let g1 = [] + let g3 = [] + let g5 = [] + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + // 过滤掉辅助线 + let list1 = _objs1.filter(obj => !obj.isGuideLine) + for (let item of list1) { + let idx = getIndex(item, canvas1) + + // 安全检查:确保 objs1[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs1[idx - 1] || typeof objs1[idx - 1].type === 'undefined')) { + continue + } + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs1[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + + // g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + // 过滤掉辅助线 + let _objs2 = canvas2.getObjects().filter(obj => !obj.isGuideLine) + let g2 = [] + let g4 = [] + let g6 = [] + left = 0 + top = 0 + black_top = 0 + black_left = 0 + all_top = 0 + all_left = 0 + // 过滤掉辅助线 + let filteredObjs2 = _objs2.filter(obj => !obj.isGuideLine) + for (let item of filteredObjs2) { + let idx = getIndex(item, canvas2) + + // 安全检查:确保 objs2[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs2[idx - 1] || typeof objs2[idx - 1].type === 'undefined')) { + continue + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //黑色字或计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if (idx != 0 && objs2[idx - 1].type == 10) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + // g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + var printPath3, printPath4; + try { + if (typeof require !== 'undefined' && typeof path !== 'undefined' && path && exePath) { + var fs = require('fs'); + var Buffer = require('buffer').Buffer; + var buffer3 = Buffer.from(url3.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + printPath3 = path.join(exePath, 'print3.png'); + fs.writeFileSync(printPath3, buffer3); + var buffer4 = Buffer.from(url4.replace(/^data:image\/\w+;base64,/, ""), 'base64'); + printPath4 = path.join(exePath, 'print4.png'); + fs.writeFileSync(printPath4, buffer4); + } else { + printPath3 = url3; + printPath4 = url4; + } + } catch (e) { + printPath3 = url3; + printPath4 = url4; + } + if (typeof printPath3 === 'undefined') printPath3 = url3; + if (typeof printPath4 === 'undefined') printPath4 = url4; + // 创建 PDF 实例 + const pdf = new jspdf.jsPDF({ + orientation: 'landscape', + unit: 'px', + format: [1012, 648] + }); + let btns = [language_str('output')] + if ((objs1.length != 0) & (objs2.length == 0)) { + //只有正面 + let img = new Image() + img.src = printPath3 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + btns.push('打印正面') + } else if ((objs1.length == 0) & (objs2.length != 0)) { + //只有背面 + let img = new Image() + img.src = printPath4 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + btns.push('打印背面') + } else { + let img = new Image() + img.src = printPath3 + + pdf.addImage(img, 'JPEG', 0, 0, 1012, 648) + + pdf.addPage() + let img2 = new Image() + img2.src = printPath4 + pdf.addImage(img2, 'JPEG', 0, 0, 1012, 648) + btns.push('打印正面') + btns.push('打印背面') + } + + var pdfBlob = pdf.output('blob') + var pdfBuffer = pdf.output('arraybuffer') + + layer.open({ + type: 1, + area: [w, h], + title: language_str('display'), //不显示标题栏"预览" + shadeClose: true, //点击遮罩关闭 + content: ` +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    `, + btn: btns, //'导出' + btn1: function (index, layero) { + if (typeof window.savePdf === 'function') { + window.savePdf(pdfBlob); + } + }, + btn2: function () { + if (window.platformBridge && window.platformBridge.printPdf && (printPath3.indexOf('data:') === 0 || printPath4.indexOf('data:') === 0)) { + if (btns[1] === '打印正面') window.platformBridge.printPdf(pdfBlob); + else if (btns[1] === '打印背面') window.platformBridge.printPdf(pdfBlob); + return; + } + if (btns[1] === '打印正面') { + printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' }); + } else if (btns[1] === '打印背面') { + printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }); + } + }, + btn3: function () { + if (window.platformBridge && window.platformBridge.printPdf) { + window.platformBridge.printPdf(pdfBlob); + return; + } + printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' }); + }, + end: function () { + // 预览窗口关闭后,恢复所有对象的 selectable 和 evented 状态 + canvas1.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine && objStates1[index]) { + obj.set({ + selectable: objStates1[index].selectable, + evented: objStates1[index].evented + }); + } + }); + canvas2.getObjects().forEach((obj, index) => { + if (!obj.isGuideLine && objStates2[index]) { + obj.set({ + selectable: objStates2[index].selectable, + evented: objStates2[index].evented + }); + } + }); + canvas1.renderAll(); + canvas2.renderAll(); + } + }) +} + +// 将 output 附加到 window 对象,确保全局可访问 +window.output = function output(callback = null, _save = save) { + if (typeof window.soonGuardMemberExport === 'function' && !window.soonGuardMemberExport()) return; + // 类型改变 + fabric.Image.fromURL(soonAsset('op_2.png'), function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + fabric.Image.fromURL(soonAsset('op_2.png'), function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + + $('#base_control').hide() + $('#line_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" + canvas1.discardActiveObject().renderAll() + canvas2.discardActiveObject().renderAll() + // 过滤掉辅助线 + let _objs1 = canvas1.getObjects().filter(obj => !obj.isGuideLine) + let g1 = [] + let g3 = [] + let g5 = [] //黑色图层 + let left = 0 //左裁剪 + let top = 0 //顶裁剪 + let black_left = 0 //黑色左裁剪 + let black_top = 0 //黑色顶裁剪 + let all_left = 0 //预览图左裁剪 + let all_top = 0 //预览图顶裁剪 + // _objs1 已经在上面过滤过辅助线了,直接使用 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + + // 安全检查:确保 objs1[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs1[idx - 1] || typeof objs1[idx - 1].type === 'undefined')) { + continue + } + //console.log("------"); + //console.log(item.getCoords()); + //console.log(getCoordsMinX(background_image.getCoords())); + //console.log(getCoordsMinX(item.getCoords())); + //console.log(getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords())); + //console.log("------"); + + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g3.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs1[idx - 1].type == 10 || objs1[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs1[idx - 1].type == 8 || objs1[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs1[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs1[idx - 1].type == 3 && item.get('fill') == '#000000') || + ( + objs1[idx - 1].type == 11 + && + item.get("fill") == "#000000" + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs1[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs1[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs1[idx - 1].black == true) //勾选并入黑色图层 + // || + // ( + // objs1[idx-1].type == 1//固定图片 + // && + // objs1[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g5.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g5.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g1.push(fabric.util.object.clone(item)) + } + let url1 = new fabric.Group(g1).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + g3.push(img1) + let url3 = new fabric.Group(g3).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url5 = new fabric.Group(g5).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + // 过滤掉辅助线 + let _objs2 = canvas2.getObjects().filter(obj => !obj.isGuideLine) + let g2 = [] + let g4 = [] + let g6 = [] //黑色图层 + left = 0 + top = 0 + black_left = 0 //黑色左裁剪 + black_top = 0 //黑色顶裁剪 + all_left = 0 //黑色左裁剪 + all_top = 0 //黑色顶裁剪 + // _objs2 已经在上面过滤过辅助线了,直接使用 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + + // 安全检查:确保 objs2[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs2[idx - 1] || typeof objs2[idx - 1].type === 'undefined')) { + continue + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > all_left) { + all_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > all_top) { + all_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g4.push(fabric.util.object.clone(item)) + if (idx != 0 && (objs2[idx - 1].type == 10 || objs2[idx - 1].type == 4)) { + continue //计数器 或 合成文字 + } + if (idx != 0 && (objs2[idx - 1].type == 8 || objs2[idx - 1].type == 9)) { + continue //二维码 条形码 + } + if (idx != 0 && objs2[idx - 1].type == 2) { + continue //合成图片 + } + if ( + idx != 0 && + ((objs2[idx - 1].type == 3 && (item.get('fill') == '#000000' || (item.get('fill') == '#ffffff' && false))) || + ( + objs2[idx - 1].type == 11 + && + (item.get("fill") == "#000000" || (item.get("fill") == "#ffffff" && false)) + //圆形文字 && 黑色 + ) || + //文字 && 黑色 + (objs2[idx - 1].type == 5 && //矩形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 6 && //圆形 + (((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('stroke') == '') || //黑填充 透明边框 + (item.get('fill') == '' && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //透明填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || //黑填充 黑边框 + ((item.get('fill') == '#000000' || item.get('fill') == '#ffffff') && item.get('strokeWidth') == 0))) || //黑填充 0边框 + (objs2[idx - 1].type == 7 && //直线 + (item.get('stroke') == '#000000' || item.get('stroke') == '#ffffff')) || + objs2[idx - 1].black == true) //勾选并入黑色图层 + // ( + // objs2[idx-1].type == 1//固定图片 + // && + // objs2[idx-1].black == true //勾选并入黑色图层 + // ) + ) { + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > black_left) { + black_left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > black_top) { + black_top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g6.push(fabric.util.object.clone(item)) + continue //黑色文字 黑色矩形 黑色线条 黑色圆形 + } else if (idx == 0) { + g6.push(fabric.util.object.clone(item)) + } + if (getCoordsMinX(background_image.getCoords()) - getCoordsMinX(item.getCoords()) > left) { + left = -(getCoordsMinX(item.getCoords()) - getCoordsMinX(background_image.getCoords())) + } + if (getCoordsMinY(background_image.getCoords()) - getCoordsMinY(item.getCoords()) > top) { + top = -(getCoordsMinY(item.getCoords()) - getCoordsMinY(background_image.getCoords())) + } + g2.push(fabric.util.object.clone(item)) + } + let url2 = new fabric.Group(g2).toDataURL({ + height: 648, + width: 1012, + top: top / zoom, + left: left / zoom + }) + let white_url1 = canvas1.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + g4.push(img2) + let url4 = new fabric.Group(g4).toDataURL({ + height: 648, + width: 1012, + top: all_top / zoom, + left: all_left / zoom + }) + let url6 = new fabric.Group(g6).toDataURL({ + height: 648, + width: 1012, + top: black_top / zoom, + left: black_left / zoom + }) + let white_url2 = canvas2.getObjects()[0].toDataURL({ + height: 648, + width: 1012 + }) + //console.log(canvas.getObjects()) + let w = document.body.clientWidth * 0.7 + 'px' + let h = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 150 + 'px' + let h1 = (document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 + 'px' + + if ((document.body.clientWidth * 0.7 - 40 - 0.06 * document.body.clientWidth * 0.7) * 0.62548 > document.body.clientHeight) { + //高度超出,则按高度计算 + h = document.body.clientHeight * 0.8 + 'px' + h1 = document.body.clientHeight * 0.8 - 150 + 'px' + w = ((document.body.clientHeight * 0.8 - 150) / 0.62548 + 40) / 0.94 + } + + let op = { frontColorPic: '', backColorPic: '', frontBlackPic: '', backBlackPic: '', frontData: [], backData: [], flag: 1, frontDisplayPic: url3, backDisplayPic: url4 } + if ((objs1.length != 0) & (objs2.length == 0)) { + op.flag = 2 + } else if ((objs1.length == 0) & (objs2.length != 0)) { + op.flag = 3 + } + op.frontColorPic = desEncrypt(url1) + op.backColorPic = desEncrypt(url2) + //op.frontBlPic = url5; + //op.backBlPic = url6; + op.frontBlackPic = desEncrypt(url5) + op.backBlackPic = desEncrypt(url6) + // _objs1 已经在上面过滤过辅助线了,直接使用 + for (let item of _objs1) { + let idx = getIndex(item, canvas1) + + // 安全检查:确保 objs1[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs1[idx - 1] || typeof objs1[idx - 1].type === 'undefined')) { + continue + } + if (idx == 0) continue //background-image + let type = objs1[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 1, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.frontData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.frontData.push({ + "type": 2, + "Text": item.get("text"), + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": "#000000", + "FontBackgroudColor": item.get("textBackgroundColor"), + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((item.get("left") - background_image1.left).toFixed(0)), + "y": parseInt((item.get("top") - background_image1.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + "circleCenter": objs1[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + //out_text + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 3, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontBackgroudColor: item.get('textBackgroundColor'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + Exceed: objs1[idx - 1].exceed, + AutoWrap: objs1[idx - 1].autoWrap, + FixSize: objs1[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs1[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 4, + name: objs1[idx - 1].name, + FontColor: objs1[idx - 1].color, + DefaultText: objs1[idx - 1].val, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + //条形码 + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 5, + name: objs1[idx - 1].name, + FontSize: parseInt(objs1[idx - 1].fontSize), + DefaultText: objs1[idx - 1].val, + HideText: objs1[idx - 1].show, + FontFamily: objs1[idx - 1].font, + FontColor: objs1[idx - 1].color, + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.frontData.push({ + type: 6, + name: objs1[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: objs1[idx - 1].start, + add: parseInt(objs1[idx - 1].add), + x: parseInt((xy[0] - background_image1.left).toFixed(0)), + y: parseInt((xy[1] - background_image1.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + //obj2 + // _objs2 已经在上面过滤过辅助线了,直接使用 + for (let item of _objs2) { + let idx = getIndex(item, canvas2) + + // 安全检查:确保 objs2[idx - 1] 存在且有 type 属性 + if (idx != 0 && (!objs2[idx - 1] || typeof objs2[idx - 1].type === 'undefined')) { + continue + } + if (idx == 0) continue //background-image + let type = objs2[idx - 1].type + if (type == 2) { + //out_pic + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 1, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].black ? '#000000' : '@', + transparency: item.get('opacity'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 3) { + //text + /*2022-07-26 黑色并入黑色图层 + if(item.get("fill") == "#000000"){ + //纯黑 + op.backData.push({ + "type" : 2, + "Text" : item.get("text"), + "FontFamily" : item.get("fontFamily"), + "FontSize" : parseInt(item.get("fontSize")), + "FontColor" : "#000000", + "FontBackgroudColor" : item.get("textBackgroundColor"), + "Bold" : item.get("fontWeight") == "bold", + "Underline" : item.get("underline"), + "Italic" : item.get("fontStyle") == "italic", + "Strikeout" : item.get("linethrough"), + "x" : parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y" : parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h" : parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w" : parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate" : parseInt(item.get("angle").toFixed(0)) + }); + } + */ + } else if (type == 11) { + //circle_text + op.backData.push({ + "type": 2, + "Text": item.get("text"), + "FontFamily": item.get("fontFamily"), + "FontSize": parseInt(item.get("fontSize")), + "FontColor": "#000000", + "FontBackgroudColor": item.get("textBackgroundColor"), + "Bold": item.get("fontWeight") == "bold", + "Underline": item.get("underline"), + "Italic": item.get("fontStyle") == "italic", + "Strikeout": item.get("linethrough"), + "x": parseInt((item.get("left") - background_image2.left).toFixed(0)), + "y": parseInt((item.get("top") - background_image2.top).toFixed(0)), + "h": parseInt((item.get("scaleY") * item.get("height")).toFixed(0)), + "w": parseInt((item.get("scaleX") * item.get("width")).toFixed(0)), + "rotate": parseInt(item.get("angle").toFixed(0)), + "circleCenter": objs2[idx - 1].circleCenter || 0 + }); + } else if (type == 4) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 3, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + DefaultText: item.get('text'), + FontSize: parseInt(item.get('fontSize')), + FontBackgroudColor: item.get('textBackgroundColor'), + FontColor: item.get('fill'), + Exceed: objs2[idx - 1].exceed, + AutoWrap: objs2[idx - 1].autoWrap, + FixSize: objs2[idx - 1].fixSize, + Bold: item.get('fontWeight') == 'bold', + Underline: item.get('underline'), + Italic: item.get('fontStyle') == 'italic', + Strikeout: item.get('linethrough'), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)), + align: objs2[idx - 1].align + }) + } else if (type == 8) { + //二维码 + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 4, + name: objs2[idx - 1].name, + FontColor: objs2[idx - 1].color, + DefaultText: objs2[idx - 1].val, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 9) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 5, + name: objs2[idx - 1].name, + DefaultText: objs2[idx - 1].val, + HideText: objs1[idx - 1].show, + FontSize: parseInt(objs2[idx - 1].fontSize), + FontFamily: objs2[idx - 1].font, + FontColor: objs2[idx - 1].color, + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } else if (type == 10) { + let xy = getAbsoluteXY(item) + op.backData.push({ + type: 6, + name: objs2[idx - 1].name, + FontFamily: item.get('fontFamily'), + FontSize: parseInt(item.get('fontSize')), + FontColor: item.get('fill'), + start: parseInt(objs2[idx - 1].start), + add: parseInt(objs2[idx - 1].add), + x: parseInt((xy[0] - background_image2.left).toFixed(0)), + y: parseInt((xy[1] - background_image2.top).toFixed(0)), + h: parseInt((item.get('scaleY') * item.get('height')).toFixed(0)), + w: parseInt((item.get('scaleX') * item.get('width')).toFixed(0)), + rotate: parseInt(item.get('angle').toFixed(0)) + }) + } + } + _save(op, callback) + //return false 开启该代码可禁止点击该按钮关闭 + }) + }) +} + +// 重新生成二维码和条形码(加载文件后调用) +// 将 regenerateQrCodesAndBarcodes 附加到 window 对象,确保全局可访问 +window.regenerateQrCodesAndBarcodes = function regenerateQrCodesAndBarcodes(canvas, objsArray) { + if (!canvas || !objsArray || !Array.isArray(objsArray)) return + + const objects = canvas.getObjects() + + // 遍历对象(跳过背景图片,索引 0) + for (let i = 1; i < objects.length; i++) { + const obj = objects[i] + if (!obj || obj.type !== 'image') continue + + const objIndex = i - 1 + if (objIndex >= objsArray.length) continue + + const objMeta = objsArray[objIndex] + if (!objMeta || (objMeta.type !== 8 && objMeta.type !== 9)) continue + + // type == 8: 二维码 - 复用 ui.js 中的生成逻辑 + if (objMeta.type === 8) { + const qr = jrQrcode.getQrBase64(objMeta.val || 'https://www.cardsoon.com', { + padding: 0, + foreground: objMeta.color || '#000000' + }) + obj.setSrc(qr, function (img) { + canvas.renderAll() + }) + } + // type == 9: 条形码 - 复用 ui.js 中的生成逻辑 + else if (objMeta.type === 9) { + JsBarcode("#barcode", objMeta.val || '123456789', { + margin: 0, + lineColor: objMeta.color || '#000000', + fontSize: objMeta.fontSize || 18, + font: objMeta.font || 'Arial', + displayValue: !objMeta.show + }); + const barcode = document.getElementById('barcode') + const bar = barcode.toDataURL("image/png") + obj.setSrc(bar, function (img) { + canvas.renderAll() + }) + } + } +} + +// 清理不需要的 src 字段(保留自定义图片的 src) +// 将 cleanupSrcFields 附加到 window 对象,确保在 ui.js 中的 saveAs 函数也能访问 +window.cleanupSrcFields = function cleanupSrcFields(jsonObjects, objsArray) { + if (!jsonObjects || !Array.isArray(jsonObjects)) return; + if (!objsArray || !Array.isArray(objsArray)) return; + + for (let i = 0; i < jsonObjects.length; i++) { + let obj = jsonObjects[i]; + // 只处理 fabric.Image 对象且有 src 字段的 + if (obj && obj.type === 'image' && obj.src) { + // 背景图片(索引 0)- 删除 src + if (i === 0) { + delete obj.src; + } + // 其他对象,通过 objs 数组判断类型 + else if (i - 1 < objsArray.length) { + let objMeta = objsArray[i - 1]; + // type == 2: 合成图片 - 删除 src + // type == 8: 二维码 - 删除 src + // type == 9: 条形码 - 删除 src + // type == 1: 自定义图片 - 必须保留 src + if (objMeta && (objMeta.type === 2 || objMeta.type === 8 || objMeta.type === 9)) { + delete obj.src; + } + // type == 1 不处理,保留 src + } + } + } +}; +// 向后兼容 +function cleanupSrcFields(jsonObjects, objsArray) { + return window.cleanupSrcFields(jsonObjects, objsArray); +} + +function applyCloudWriteResult(result, fallbackName) { + if (result && result.fileKey) { + openAs.name = result.fileKey; + return result.fileKey; + } + return fallbackName; +} + +function cloudSaveSuccessLabel(res, fallback) { + if (res && res.name) return res.name; + if (typeof window.soonDisplayFileName === 'function') { + return window.soonDisplayFileName(openAs.name || fallback); + } + return fallback || ''; +} + +function onCloudWriteDone(res, fallback, callback) { + applyCloudWriteResult(res, fallback); + layer.msg(language_str('saveSucc') + cloudSaveSuccessLabel(res, fallback)); + if (callback && typeof callback === 'function') callback(); +} + +function guardWebSave() { + return typeof window.soonGuardCloudSave === 'function' ? window.soonGuardCloudSave() : true; +} + +function saveAs(op1, callback) { + // 临时移除辅助线,保存后再恢复 + let guideLines1 = [] + let guideLines2 = [] + canvas1.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines1.push(obj) + canvas1.remove(obj) + } + }) + canvas2.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines2.push(obj) + canvas2.remove(obj) + } + }) + + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented']; + let j = canvas1.toJSON(props) + let b = canvas2.toJSON(props) + + // 恢复辅助线 + guideLines1.forEach(obj => canvas1.add(obj)) + guideLines2.forEach(obj => canvas2.add(obj)) + canvas1.renderAll() + canvas2.renderAll() + + // 清理不需要的 src 字段 + cleanupSrcFields(j.objects, objs1); + cleanupSrcFields(b.objects, objs2); + + let o = { f: j, b: b, fo: objs1, bo: objs2 } + let con_o = Object.assign(o, op1) + if (!guardWebSave()) return; + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showSaveDialog({ title: language_str('saveFile'), filters: [{ name: 'Soon File Type', extensions: ['soon'] }], defaultPath: openAs.name || undefined }) + .then(function(result) { + if (result.canceled) return; + var fp = result.filePath || (result.fileHandle && result.fileHandle.name); + if (!fp) return; + var ext = (typeof path !== 'undefined' && path && path.extname) ? path.extname(fp) : (fp.indexOf('.') >= 0 ? fp.slice(fp.lastIndexOf('.')) : ''); + if (ext !== '.soon') fp = (fp || 'design').replace(/\.soon$/i, '') + '.soon'; + con_o.soonType = 2; + var content = JSON.stringify(con_o); + if (window.platformBridge && window.platformBridge.writeFile) { + window.platformBridge.writeFile(fp, content).then(function (res) { + onCloudWriteDone(res, fp, callback); + }).catch(function () {}); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFileSync(fp, content, 'utf8'); + openAs.name = fp; + layer.msg(language_str('saveSucc') + openAs.name); + saveHistory(); + if (callback && typeof callback === 'function') callback(); + } + }) + .catch(function(err) {}); +} + +function save(op1, callback) { + // 临时移除辅助线,保存后再恢复 + let guideLines1 = [] + let guideLines2 = [] + canvas1.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines1.push(obj) + canvas1.remove(obj) + } + }) + canvas2.getObjects().forEach((obj, index) => { + if (obj.isGuideLine) { + guideLines2.push(obj) + canvas2.remove(obj) + } + }) + + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented']; + let j = canvas1.toJSON(props) + let b = canvas2.toJSON(props) + + // 恢复辅助线 + guideLines1.forEach(obj => canvas1.add(obj)) + guideLines2.forEach(obj => canvas2.add(obj)) + canvas1.renderAll() + canvas2.renderAll() + + // 清理不需要的 src 字段 + cleanupSrcFields(j.objects, objs1); + cleanupSrcFields(b.objects, objs2); + + let o = { f: j, b: b, fo: objs1, bo: objs2 } + let con_o = Object.assign(o, op1) + con_o.soonType = 2 + var content = JSON.stringify(con_o) + if (!guardWebSave()) return; + if (openAs.name != '') { + if (window.platformBridge && window.platformBridge.writeFile) { + window.platformBridge.writeFile(openAs.name, content).then(function (res) { + onCloudWriteDone(res, openAs.name, callback); + }).catch(function () {}); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFileSync(openAs.name, content, 'utf8'); + layer.msg(language_str('saveSucc') + openAs.name); + saveHistory(); + if (callback && typeof callback === 'function') callback(); + } + return; + } + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showSaveDialog({ title: language_str('saveFile'), filters: [{ name: 'Soon File Type', extensions: ['soon'] }], defaultPath: openAs.name || undefined }) + .then(function(result) { + if (result.canceled) return; + var fp = result.filePath || (result.fileHandle && result.fileHandle.name); + if (!fp) return; + var ext = (typeof path !== 'undefined' && path && path.extname) ? path.extname(fp) : (fp.indexOf('.') >= 0 ? fp.slice(fp.lastIndexOf('.')) : ''); + if (ext !== '.soon') fp = (fp || 'design').replace(/\.soon$/i, '') + '.soon'; + if (window.platformBridge && window.platformBridge.writeFile) { + window.platformBridge.writeFile(fp, content).then(function (res) { + onCloudWriteDone(res, fp, callback); + }).catch(function () {}); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs) { + fs.writeFileSync(fp, content, 'utf8'); + openAs.name = fp; + layer.msg(language_str('saveSucc') + openAs.name); + saveHistory(); + if (callback && typeof callback === 'function') callback(); + } + }) + .catch(function(err) {}); +} + +window.saveHistory = function saveHistory() { + if (typeof window.soonIsWebPortal === 'function' && window.soonIsWebPortal()) return; + function doWrite(j) { + var currentPath = openAs.name; + if (!currentPath) return; + var pathForHistory = currentPath; + if (window.platformBridge && currentPath.indexOf('soondesign_session:') !== 0 && currentPath.indexOf('soondesign_file:') !== 0) { + pathForHistory = 'soondesign_session:' + (currentPath || 'design.soon'); + } + var list = Array.isArray(j.history) ? j.history : []; + var newList = list.filter(function(item) { return item.path !== pathForHistory && item.path !== currentPath; }); + newList.unshift({ time: getDate(), path: pathForHistory, type: 2 }); + if (newList.length > 20) newList = newList.slice(0, 20); + j.history = newList; + if (window.platformBridge && window.platformBridge.writeHistory) { + window.platformBridge.writeHistory(j); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (fs && typeof fullPath !== 'undefined' && fullPath) { + try { fs.writeFileSync(fullPath, JSON.stringify(j), 'utf8'); } catch (e) {} + } + } + if (window.platformBridge && window.platformBridge.readHistory) { + window.platformBridge.readHistory().then(function(j) { + if (!j || !Array.isArray(j.history)) j = { history: [] }; + doWrite(j); + }).catch(function() { doWrite({ history: [] }); }); + return; + } + var fs = typeof require !== 'undefined' ? require('fs') : null; + var j = { history: [] }; + if (fs && typeof fullPath !== 'undefined' && fullPath) { + try { + var fsData = fs.readFileSync(fullPath); + if (fsData && fsData.toString) j = JSON.parse(fsData.toString()); + } catch (e) { j = { history: [] }; } + if (!Array.isArray(j.history)) j.history = []; + } + doWrite(j); +}; +// 向后兼容 +function saveHistory() { + return window.saveHistory(); +} + +// 使用 window.openFile 避免与浏览器原生的 window.open 冲突 +window.openFile = function open(file, jAlready) { + if (!file) return; + if (typeof window.soonBindFileMeta === 'function') window.soonBindFileMeta(file); + function doOpenWithJson(j) { + if (!j || !background_image) return; + openAs.name = file; + var left = background_image.left + let top = background_image.top + let new_left = j.f.objects[0].left + let new_top = j.f.objects[0].top + for (let o of j.f.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + for (let o of j.b.objects) { + o.left = o.left - new_left + left + o.top = o.top - new_top + top + } + // 如果背景图片没有src,从路径恢复(因为保存时删除了src以减小文件大小) + if (j.f.objects[0] && !j.f.objects[0].src) { + j.f.objects[0].src = soonAsset('bg_front_2.png') + } + if (j.b.objects[0] && !j.b.objects[0].src) { + j.b.objects[0].src = soonAsset('bg_back_2.png') + } + + // 为二维码和条形码对象创建临时的 src(Fabric.js 需要 src 来创建 Image 对象) + // 加载完成后会重新生成,所以这里只需要一个占位符 + const fo = j.fo || [] + const bo = j.bo || [] + const placeholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + + // 为正面二维码、条形码和合成图片创建临时 src + fo.forEach((objMeta, idx) => { + if ((objMeta.type === 8 || objMeta.type === 9) && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) { + j.f.objects[idx + 1].src = placeholder + } else if (objMeta.type === 2 && j.f.objects[idx + 1] && !j.f.objects[idx + 1].src) { + // 合成图片类型,恢复默认图片 + j.f.objects[idx + 1].src = soonAsset('outpic_extra.png') + } + }) + + // 为背面二维码、条形码和合成图片创建临时 src + bo.forEach((objMeta, idx) => { + if ((objMeta.type === 8 || objMeta.type === 9) && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) { + j.b.objects[idx + 1].src = placeholder + } else if (objMeta.type === 2 && j.b.objects && j.b.objects[idx + 1] && !j.b.objects[idx + 1].src) { + // 合成图片类型,恢复默认图片 + j.b.objects[idx + 1].src = soonAsset('outpic_extra.png') + } + }) + + // 先清空历史记录和重置 step + objs2 = j.bo || [] + objs1 = j.fo || [] + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + + // 同步更新 window 变量 + window.objs1 = objs1 + window.objs2 = objs2 + window.step1 = step1 + window.step2 = step2 + window.recordJson1 = recordJson1 + window.recordJson2 = recordJson2 + window.recordObjs1 = recordObjs1 + window.recordObjs2 = recordObjs2 + + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + window.objs = objs1 + window.recordObjs = recordObjs1 + window.recordJson = recordJson1 + window.step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + background_image = background_image2 + step = step2 + window.objs = objs2 + window.recordObjs = recordObjs2 + window.recordJson = recordJson2 + window.step = step2 + } + + // 加载 JSON,并在回调中重新生成二维码和条形码 + // 使用 Promise 确保两个画布都加载完成后再保存初始状态 + let canvas1Loaded = false + let canvas2Loaded = false + + function saveInitialState() { + // 两个画布都加载完成后才保存初始状态 + if (!canvas1Loaded || !canvas2Loaded) return + + // 确保使用正确的 objs1 和 objs2(从文件加载的 fo 和 bo) + // 注意:必须使用 fo 和 bo,因为它们是直接从文件 JSON 中读取的 + // 同时确保 objs1 和 objs2 也被正确设置 + objs1 = fo || [] + objs2 = bo || [] + window.objs1 = objs1 + window.objs2 = objs2 + + // 更新背景图引用 + background_image1 = canvas1.getObjects()[0] + background_image2 = canvas2.getObjects()[0] + window.background_image1 = background_image1 + window.background_image2 = background_image2 + + if ($('#front_side').hasClass('ui-button-active')) { + background_image = background_image1 + window.background_image = background_image1 + objs = objs1 + window.objs = objs1 + } else if ($('#back_side').hasClass('ui-button-active')) { + background_image = background_image2 || background_image1 + window.background_image = background_image + objs = objs2 + window.objs = objs2 + } + + // 保存初始状态(使用从文件加载的 objs 数组) + recordObjs1.push(JSON.stringify(objs1)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + let j1 = canvas1.toJSON(props) + j1.objects[0].hoverCursor = 'default' + recordJson1.push(j1) + + recordObjs2.push(JSON.stringify(objs2)) + let j2 = canvas2.toJSON(props) + j2.objects[0].hoverCursor = 'default' + recordJson2.push(j2) + + // 同步更新 window 变量和局部变量 + window.recordObjs1 = recordObjs1 + window.recordJson1 = recordJson1 + window.recordObjs2 = recordObjs2 + window.recordJson2 = recordJson2 + // 确保局部变量也同步 + if ($('#front_side').hasClass('ui-button-active')) { + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + } + } + + canvas1.loadFromJSON(j.f, function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas1); + } + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas1, fo); + } + // 加载后更新列表和图标显示 + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas1.renderAll() + canvas1Loaded = true + saveInitialState() + }) + canvas2.loadFromJSON(j.b, function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas2); + } + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas2, bo); + } + // 加载后更新列表和图标显示 + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas2.renderAll() + canvas2Loaded = true + saveInitialState() + }) + } + if (arguments.length >= 2 && jAlready) { + doOpenWithJson(jAlready); + return; + } + if (window.platformBridge && window.platformBridge.readJsonFile) { + return window.platformBridge.readJsonFile(file).then(function(j) { + if (j) { + doOpenWithJson(j); + openAs.name = file; + } + }); + } + try { + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (!fs || file.indexOf('soondesign_session:') === 0) return; + var fsData = fs.readFileSync(file); + var j = JSON.parse(fsData.toString()); + doOpenWithJson(j); + } catch (e) {} +}; + +function clearAll() { + //let blank = "{\"f\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"#000000\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"b\":{\"version\":\"4.6.0\",\"objects\":[{\"type\":\"image\",\"version\":\"4.6.0\",\"originX\":\"left\",\"originY\":\"top\",\"left\":253.5,\"top\":121.5,\"width\":1012,\"height\":648,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeDashOffset\":0,\"strokeLineJoin\":\"miter\",\"strokeUniform\":false,\"strokeMiterLimit\":4,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"paintFirst\":\"fill\",\"globalCompositeOperation\":\"source-over\",\"skewX\":0,\"skewY\":0,\"cropX\":0,\"cropY\":0,\"selectable\":false,\"hoverable\":false,\"hoverCursor\":\"default\",\"src\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAwAAAKICAYAAAD0EmiCAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3W3I5Xldx/HvT3SLdk0ljFyN0t1IEjN2TMEHZXcoKEjmQ9HKUkgURQtv8pGlVN6UlqBJN+Kzwhuo0MjKHhi27pJJUbG7IesNJOUurt2s6T/+cmYZd3bmc12zM7M783kd2Acy3+uc831954lvzpxrzX3w2LbtATPz+Jm5bmYeNzPXzszVM/NtM3Pl4b+r7oO35iUJECBAgAABAgQIECBAgMDFFrhjZr58+O8/ZuZzM3PTzPzTzNw4M/+w1vraxX5T62K94LZtj56ZZ8zMj87MDx3iwMV6ea9DgAABAgQIECBAgAABAgQuVYH/nJmPzsxfzsyfrrX+7WIsckGDwbZtD5mZ587M82bmSRdjIa9BgAABAgQIECBAgAABAgQuc4G/m5n3zMx711q3X6hdL0gw2LbtETPzypl54cz4pwUX6nqelwABAgQIECBAgAABAgSaBfZ/yvCumXnTWuvz5xvivAaDQyh43cz8zMx88/l+s56PAAECBAgQIECAAAECBAgQOE3gf2bm92fm9eczHJyXYLBt24MOnyh4jU8U+KtLgAABAgQIECBAgAABAgTuE4H9EwdvOHzi4Cv39h3c62CwbduJQ8nYf+uBBwECBAgQIECAAAECBAgQIHDfCnxqZn56rbX/hoVzfpxzMNi2bf/ZV8zMr87MFef8DvwgAQIECBAgQIAAAQIECBAgcL4F7pyZ187Mm9da27k8+TkFg23bHjwzfzAzzz6XF/UzBAgQIECAAAECBAgQIECAwEUReP/MPH+t9aXjvtqxg8G2bY+cmT+ZmR847ouZJ0CAAAECBAgQIECAAAECBC66wN/PzDPXWp89zisfKxhs23bNzPzFzHz3cV7ELAECBAgQIECAAAECBAgQIHCfCnx6Zn5srXXzUd/FkYPBtm2Pnpm/mZlHHfXJzREgQIAAAQIECBAgQIAAAQL3G4HPzMwPr7VuOco7OlIw2Lbt4TPzsZm59ihPaoYAAQIECBAgQIAAAQIECBC4XwrcNDNPWWt9Ib27GAy2bbtyZj4yM09OT+bPCRAgQIAAAQIECBAgQIAAgfu9wMdn5ifSFyGeNRgcfnXiH83MT93v1/UGCRAgQIAAAQIECBAgQIAAgaMKvG9mnnO2X7mYgsErZ+Y3jvpq5ggQIECAAAECBAgQIECAAIFLRuCX1lpn/P/8ZwwG27ZdNzN/OzNXXDKreqMECBAgQIAAAQIECBAgQIDAUQXuPHyfwQ339AP3GAy2bXvQzHxiZr7/qK9ijgABAgQIECBAgAABAgQIELjkBD41MyfWWl+5+zs/UzB41cy88ZJb0xsmQIAAAQIECBAgQIAAAQIEjivwmrXWaQ3gtGCwbdvVM/MvM3PVcV/BPAECBAgQIECAAAECBAgQIHDJCdwxM49da3321Hd+T8HgnTPzwktuPW+YAAECBAgQIECAAAECBAgQOFeBd621XnTGYLBt26MPny7Yv8PAgwABAgQIECBAgAABAgQIEOgQ2L/DYP+UwS0n1/2GTxhs2/a2mXlJh4UtCRAgQIAAAQIECBAgQIAAgVME3r7WeulpwWDbtofOzGdm5kpcBAgQIECAAAECBAgQIECAQJ3Af83MI9dat+2b3/UJg23bfmFmfqeOw8IECBAgQIAAAQIECBAgQIDASYEXr7XecfdgcP3MPJERAQIECBAgQIAAAQIECBAgUCtw/VrrSXcFg23brpmZm2o5LE6AAAECBAgQIECAAAECBAicFLhm//LDr/+ThG3bXjYzb2VDgAABAgQIECBAgAABAgQI1Au8bK31WyeDwYdm5mn1JAAIECBAgAABAgQIECBAgACBD6+1nr62bXvgzHxxZq5iQoAAAQIECBAgQIAAAQIECNQL3DEzD9uDwXUzc0M9BwACBAgQIECAAAECBAgQIEDgpMCJPRi8YGbezYQAAQIECBAgQIAAAQIECBAgcBB4wR4M3jIzL0dCgAABAgQIECBAgAABAgQIEDgIvHUPBh+YmWchIUCAAAECBAgQIECAAAECBAgcBD64B4P9+wv27zHwIECAAAECBAgQIECAAAECBAjsAjfuweBfZ+Z7eBAgQIAAAQIECBAgQIAAAQIEDgK37sHg8zPzHUgIECBAgAABAgQIECBAgAABAgeB2/dgsP9+xSuRECBAgAABAgQIECBAgAABAgQOAl/dg8GGgwABAgQIECBAgAABAgQIECBwqoBg4O8DAQIECBAgQIAAAQIECBAgcJqAYOAvBQECBAgQIECAAAECBAgQICAY+DtAgAABAgQIECBAgAABAgQIZAGfMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIEDpfCiFAAAcY0lEQVSAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAUEg2xkggABAgQIECBAgAABAgQI1AkIBnUntzABAgQIECBAgAABAgQIEMgCgkE2MkGAAAECBAgQIECAAAECBOoEBIO6k1uYAAECBAgQIECAAAECBAhkAcEgG5kgQIAAAQIECBAgQIAAAQJ1AoJB3cktTIAAAQIECBAgQIAAAQIEsoBgkI1MECBAgAABAgQIECBAgACBOgHBoO7kFiZAgAABAgQIECBAgAABAllAMMhGJggQIECAAAECBAgQIECAQJ2AYFB3cgsTIECAAAECBAgQIECAAIEsIBhkIxMECBAgQIAAAQIECBAgQKBOQDCoO7mFCRAgQIAAAQIECBAgQIBAFhAMspEJAgQIECBAgAABAgQIECBQJyAY1J3cwgQIECBAgAABAgQIECBAIAsIBtnIBAECBAgQIECAAAECBAgQqBMQDOpObmECBAgQIECAAAECBAgQIJAFBINsZIIAAQIECBAgQIAAAQIECNQJCAZ1J7cwAQIECBAgQIAAAQIECBDIAoJBNjJBgAABAgQIECBAgAABAgTqBASDupNbmAABAgQIECBAgAABAgQIZAHBIBuZIECAAAECBAgQIECAAAECdQKCQd3JLUyAAAECBAgQIECAAAECBLKAYJCNTBAgQIAAAQIECBAgQIAAgToBwaDu5BYmQIAAAQIECBAgQIAAAQJZQDDIRiYIECBAgAABAgQIECBAgECdgGBQd3ILEyBAgAABAgQIECBAgACBLCAYZCMTBAgQIECAAAECBAgQIECgTkAwqDu5hQkQIECAAAECBAgQIECAQBYQDLKRCQIECBAgQIAAAQIECBAgUCcgGNSd3MIECBAgQIAAAQIECBAgQCALCAbZyAQBAgQIECBAgAABAgQIEKgTEAzqTm5hAgQIECBAgAABAgQIECCQBQSDbGSCAAECBAgQIECAAAECBAjUCQgGdSe3MAECBAgQIECAAAECBAgQyAKCQTYyQYAAAQIECBAgQIAAAQIE6gQEg7qTW5gAAQIECBAgQIAAAQIECGQBwSAbmSBAgAABAgQIECBAgAABAnUCgkHdyS1MgAABAgQIECBAgAABAgSygGCQjUwQIECAAAECBAgQIECAAIE6AcGg7uQWJkCAAAECBAgQIECAAAECWUAwyEYmCBAgQIAAAQIECBAgQIBAnYBgUHdyCxMgQIAAAQIECBAgQIAAgSwgGGQjEwQIECBAgAABAgQIECBAoE5AMKg7uYUJECBAgAABAgQIECBAgEAWEAyykQkCBAgQIECAAAECBAgQIFAnIBjUndzCBAgQIECAAAECBAgQIEAgCwgG2cgEAQIECBAgQIAAAQIECBCoExAM6k5uYQIECBAgQIAAAQIECBAgkAX2YPDVmXlAHjVBgAABAgQIECBAgAABAgQIlAh8bQ8Gt83MQ0oWtiYBAgQIECBAgAABAgQIECCQBW7fg8GtM/OoPGuCAAECBAgQIECAAAECBAgQKBG4dQ8GN8zMdSULW5MAAQIECBAgQIAAAQIECBDIAjfuweADM/OsPGuCAAECBAgQIECAAAECBAgQKBH44B4M3jIzLy9Z2JoECBAgQIAAAQIECBAgQIBAFnjrHgxeMDPvzrMmCBAgQIAAAQIECBAgQIAAgRKBn9uDwYmZ+UTJwtYkQIAAAQIECBAgQIAAAQIEssCJPRg8cGa+ODNX5XkTBAgQIECAAAECBAgQIECAwGUucMfMPGztS27b9qGZedplvrD1CBAgQIAAAQIECBAgQIAAgSzw4bXW008Gg/1LD/cvP/QgQIAAAQIECBAgQIAAAQIEugVevtb6zZPB4JqZuanbw/YECBAgQIAAAQIECBAgQIDAzFy71rr568Fgf2zbdv3MPBENAQIECBAgQIAAAQIECBAgUCtw/VrrSfv2pwaDF8/Mb9eSWJwAAQIECBAgQIAAAQIECBB48VrrHXcPBg+dmc/OzLfwIUCAAAECBAgQIECAAAECBOoE/ntmrl5r3fYNwWD/H9u2vW1mXlJHYmECBAgQIECAAAECBAgQIEDg7Wutl55kuOufJByCwWNm5p9n5kGcCBAgQIAAAQIECBAgQIAAgRqB/5uZ711r3XKPweAQDd41Mz9fQ2JRAgQIECBAgAABAgQIECBA4HfXWi88leEbPmFwCAaPPHzK4CpeBAgQIECAAAECBAgQIECAwGUvcMfMPHattX+v4V2P04LBIRq8embecNmTWJAAAQIECBAgQIAAAQIECBB47VrrtAZwpmCwf4fBDTPzeG4ECBAgQIAAAQIECBAgQIDAZSvwqZk5sdb6yt03vMdgsA9t23ZiZj42M1dctiwWI0CAAAECBAgQIECAAAECvQJ3zsxT1lr7BwZOe5wxGByiwS/OzK/32tmcAAECBAgQIECAAAECBAhctgKvWmv92pm2S8Fg//M/nplnX7Y8FiNAgAABAgQIECBAgAABAn0C75uZ56y1tnMKBvsPbdv2rTPz5zPz5D4/GxMgQIAAAQIECBAgQIAAgctO4OMz87S11u1n2+ysnzA4+YPbtj388H0G1152TBYiQIAAAQIECBAgQIAAAQI9AjcdvrfgC2nlIwWD/Um2bXvMzHx0Zh6VntSfEyBAgAABAgQIECBAgAABAvc7gc/MzFPXWjcf5Z0dORgcosE1M/ORmfmuozy5GQIECBAgQIAAAQIECBAgQOB+IfDpmfnxtdb+CYMjPY4VDA7RYP+EwZ/MzBOO9AqGCBAgQIAAAQIECBAgQIAAgftS4JMz88y11v4JgyM/jh0MDtHgwTPzhzPzk0d+JYMECBAgQIAAAQIECBAgQIDAxRZ4/8w8f631peO+8DkFg0M02H/2lTPzKzNzxXFf2DwBAgQIECBAgAABAgQIECBwwQTunJlfnpk3ne1XJ57t1c85GJx80m3bThw+bfC4C7amJyZAgAABAgQIECBAgAABAgSOKvCPM/O8tdaNR/2Be5q718Fgf9Jt2/ZPGOyfNnj1zFx1b96QnyVAgAABAgQIECBAgAABAgTOSeCOmXnj4VMF+ycM7tXjvASDk+9g27ZHzMzrZuZnZ+ab7tU788MECBAgQIAAAQIECBAgQIDAUQT+d2Z+b2Zev9b6/FF+4Cgz5zUYnBIOrp6ZV8zMi2bmyqO8ETMECBAgQIAAAQIECBAgQIDAsQS+PDPvnJk3r7U+d6yfPMLwBQkGp4SDh87Mc/d/OzEzP3iE92OEAAECBAgQIECAAAECBAgQOLvA9TPznpl571rrtguFdUGDwalvetu2x8zMM2bmR2bmqTPzsAu1lOclQIAAAQIECBAgQIAAAQKXkcAXZ+avZ+avZubP1lo3X4zdLlowuFs8eMDMPGFmrpuZ75uZa2bmO2fm2w9fmrh/MsGDAAECBAgQIECAAAECBAi0COyfFNi/tPDfZ+bWmbllZvbfdrD/poNPrrW+drEh/h+5oaPdsxLhuQAAAABJRU5ErkJggg==\",\"crossOrigin\":null,\"filters\":[]}],\"hoverCursor\":\"move\"},\"fo\":[],\"bo\":[]}"; + //let j = JSON.parse(blank); + //canvas1.setZoom(1); + //canvas2.setZoom(1); + //canvas1.loadFromJSON(j.f); + //canvas2.loadFromJSON(j.b); + let t_objs1 = canvas1.getObjects() + let t_objs2 = canvas2.getObjects() + for (let i = 1; i < t_objs1.length; i++) { + canvas1.remove(t_objs1[i]) + } + for (let i = 1; i < t_objs2.length; i++) { + canvas2.remove(t_objs2[i]) + } + objs2 = [] + objs1 = [] + step1.val = 0 + step2.val = 0 + recordJson1 = [] + recordJson2 = [] + recordObjs1 = [] + recordObjs2 = [] + if ($('#front_side').hasClass('ui-button-active')) { + objs = objs1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + } else if ($('#back_side').hasClass('ui-button-active')) { + objs = objs2 + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + } + openAs.name = '' + canvas1.renderAll() + canvas2.renderAll() + // 调用 updateList,现在它已附加到 window 对象 + if (typeof window.updateList === 'function') { + window.updateList(); + } else { + // 延迟检查,确保 core.js 已加载 + setTimeout(() => { + if (typeof window.updateList === 'function') { + window.updateList(); + } + }, 100); + } +} + +// 将 clearAll 附加到 window 对象,确保全局可访问 +window.clearAll = clearAll; + diff --git a/frontend-web/js/design2/ui.js b/frontend-web/js/design2/ui.js new file mode 100644 index 0000000..965f524 --- /dev/null +++ b/frontend-web/js/design2/ui.js @@ -0,0 +1,2773 @@ +var _keyctrl = false, + _keyc = false, + _textEdit = false, + _inputEdit = false +$('input').focus(function () { + _inputEdit = true +}) +$('input').blur(function () { + _inputEdit = false +}) +$('textarea').focus(function () { + _inputEdit = true +}) +$('textarea').blur(function () { + _inputEdit = false +}) +$('body').keyup(function (event) { + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放 + _keyctrl = false + } + if (event.keyCode == 67) { + _keyc = false + } +}) +$('body').keydown(function (event) { + // 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta) + const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端 + + // 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的) + // 注意:大部分快捷键已在 core.js 中统一处理,此处只保留 ui.js 特有的快捷键 + // 已移除的重复快捷键:CTRL/CMD+N/O/S/Z/Y/C/V/SHIFT+DEL/DEL(已在 core.js 中处理) + if (!_inputEdit && !_textEdit) { + // ui.js 特有的快捷键可以在这里添加 + } + // 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta) + if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd) + _keyctrl = true + if (_keyc && _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + } + } + if (event.keyCode == 67) { + _keyc = true + if (_keyctrl & _textEdit) { + //同时按下且在编辑状态 + setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1) + } + } + if (event.keyCode == 37) { + //left + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + if (canvas.getActiveObject()) { + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 38) { + //up + let v = -1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = -5 + } + if (canvas.getActiveObject()) { + let y = canvas.getActiveObject().get('top') + y += v + canvas.getActiveObject().set('top', y) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 39) { + //right + let v = 1 + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5 + } + if (canvas.getActiveObject()) { + let x = canvas.getActiveObject().get('left') + x += v + canvas.getActiveObject().set('left', x) + canvas.renderAll() + recordState() + updateControls() + } + } + if (event.keyCode == 40) { + //down + let v = 1; + if (_keyctrl || event.ctrlKey || event.metaKey) { + v = 5; + } + const currentCanvas = window.canvas || canvas; + if (currentCanvas && currentCanvas.getActiveObject()) { + let y = currentCanvas.getActiveObject().get('top'); + y += v; + currentCanvas.getActiveObject().set('top', y); + currentCanvas.renderAll(); + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + if (typeof updateControls === 'function') { + updateControls(); + } + } + } + // 普通删除键(DEL键,没有按CTRL/CMD) + if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) { + // 使用 window.canvas 作为后备,确保在 eval() 加载 .jsc 时也能访问 + const currentCanvas = window.canvas || canvas; + const currentObjs = window.objs || objs; + if (!currentCanvas) { + return; + } + let actObjs = currentCanvas.getActiveObjects() + let allObjs = currentCanvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item, currentCanvas) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + currentObjs.splice(i - 1, 1) + currentCanvas.remove(item) + flag = 1 + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + currentCanvas.discardActiveObject() + if (flag == 1) { + if (typeof window.recordState === 'function') { + window.recordState(); + } else if (typeof recordState === 'function') { + recordState(); + } + currentCanvas.renderAll(); + if (typeof window.handleObjectSelected === 'function') { + window.handleObjectSelected(); + } + } + } +}) +// pre_add_image 已在 design2.js 中定义为全局变量 +$('#addPic').click(function () { + OpenDialog() + function OpenDialog() { + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function (opts) { return window.platformBridge.showOpenDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi + .showOpenDialog({ + title: language_str('selectPic'), //请选择图片 + buttonLabel: language_str('comf'), //"确认" + filters: [{ name: 'Image File Type', extensions: ['png', 'jpg', 'jpeg'] }] + }) + .then((result) => { + if (result.canceled) return + var url = (result.files && result.files[0]) ? URL.createObjectURL(result.files[0]) : (result.filePaths && result.filePaths[0]); + if (!url) return + fabric.Image.fromURL(url, function (image) { + //image.left = pointer.x; + image.setSrc(image.toDataURL(), function (image) { + pre_add_image = image + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 1 + }) + //canvas.add(image); + }) + }) + .catch((err) => { + }) + } +}) +$('#addOutPic').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 2 +}) +$('#addText').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 3 +}) +$("#addCircleText").click(function () { + background_image.hoverCursor = "crosshair"; + $(this).css("background-color", "#646E76"); + + $("#addPic").css("background-color", ""); + $("#addOutPic").css("background-color", ""); + $("#addText").css("background-color", ""); + $("#addOutText").css("background-color", ""); + $("#addRect").css("background-color", ""); + $("#addCircle").css("background-color", ""); + $("#addLine").css("background-color", ""); + $("#addQrCode").css("background-color", ""); + $("#addBarCode").css("background-color", ""); + $("#addCounter").css("background-color", ""); + addState = 11; +}); +$('#addOutText').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 4 +}) +$('#addRect').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 5 +}) +$('#addCircle').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 6 +}) +$('#addLine').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 7 +}) +$('#addQrCode').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addBarCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 8 +}) +$('#addBarCode').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addCounter').css('background-color', '') + addState = 9 +}) +$('#addCounter').click(function () { + background_image.hoverCursor = 'crosshair' + $(this).css('background-color', '#646E76') + + $('#addPic').css('background-color', '') + $('#addOutPic').css('background-color', '') + $('#addText').css('background-color', '') + $("#addCircleText").css("background-color", ""); + $('#addOutText').css('background-color', '') + $('#addRect').css('background-color', '') + $('#addCircle').css('background-color', '') + $('#addLine').css('background-color', '') + $('#addQrCode').css('background-color', '') + $('#addBarCode').css('background-color', '') + addState = 10 +}) +$('#version_change').click(function () { + if (bg_version == 1) { + bg_version = 2 + } else { + bg_version = 1 + } + // 类型改变 + $('#source_front').attr('src', soonAsset('front_bg') + bg_version + '_2.png') + canvas.renderAll() +}) +$('#front_side').click(function () { + $(this).addClass('ui-button-active') + $('#back_side').removeClass('ui-button-active') + $('.canvas-container:eq(0)').show() + $('.canvas-container:eq(1)').hide() + canvas = canvas1 + objs = objs1 + background_image = background_image1 + window.canvas = canvas; + window.objs = objs; + window.background_image = background_image; + next_objs = next_objs1 + pre_objs = pre_objs1 + next_json = next_json1 + pre_json = pre_json1 + recordObjs = recordObjs1 + recordJson = recordJson1 + step = step1 + // 同步到 window 对象,确保 recordState 使用正确的数组 + window.recordObjs = recordObjs1 + window.recordJson = recordJson1 + window.step = step1 + if (typeof window.updateList === 'function') { + window.updateList(); + } + + $('#line_control').hide() + $('#base_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //背景 +}) +$('#back_side').click(function () { + $(this).addClass('ui-button-active') + $('#front_side').removeClass('ui-button-active') + $('.canvas-container:eq(0)').hide() + $('.canvas-container:eq(1)').show() + canvas = canvas2 + objs = objs2 + background_image = background_image2 + window.canvas = canvas; + window.objs = objs; + window.background_image = background_image; + next_objs = next_objs2 + pre_objs = pre_objs2 + next_json = next_json2 + pre_json = pre_json2 + recordObjs = recordObjs2 + recordJson = recordJson2 + step = step2 + // 同步到 window 对象,确保 recordState 使用正确的数组 + window.recordObjs = recordObjs2 + window.recordJson = recordJson2 + window.step = step2 + if (typeof window.updateList === 'function') { + window.updateList(); + } + $('#line_control').hide() + $('#base_control').hide() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() + $('#component_type').text(language_str('bg')) //"背景" +}) +$('#top_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个,对齐到背景顶部 + let obj = _objs[0] + obj.setCoords() + let coords = obj.getCoords(true) + let objMinY = getCoordsMinY(coords) + let bgMinY = getCoordsMinY(background_image.getCoords(true)) + let deltaY = bgMinY - objMinY + obj.set('top', obj.get('top') + deltaY) + obj.setCoords() + canvas.renderAll() + recordState() + return + } + // 多个对象对齐:找到所有对象边界框的最小Y值 + let minTop = Infinity + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinY = getCoordsMinY(coords) + if (itemMinY < minTop) { + minTop = itemMinY + } + } + // 将所有对象对齐到最小Y值 + groupItems = [] + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinY = getCoordsMinY(coords) + let deltaY = minTop - itemMinY + item.set('top', item.get('top') + deltaY) + item.setCoords() + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + recordState() +}) +$('#bottom_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个,对齐到背景底部 + let obj = _objs[0] + obj.setCoords() + let coords = obj.getCoords(true) + let objMaxY = getCoordsMaxY(coords) + let bgMaxY = getCoordsMaxY(background_image.getCoords(true)) + let deltaY = bgMaxY - objMaxY + obj.set('top', obj.get('top') + deltaY) + obj.setCoords() + canvas.renderAll() + recordState() + return + } + // 多个对象对齐:找到所有对象边界框的最大Y值 + let maxBottom = -Infinity + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMaxY = getCoordsMaxY(coords) + if (itemMaxY > maxBottom) { + maxBottom = itemMaxY + } + } + // 将所有对象对齐到最大Y值 + groupItems = [] + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMaxY = getCoordsMaxY(coords) + let deltaY = maxBottom - itemMaxY + item.set('top', item.get('top') + deltaY) + item.setCoords() + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + recordState() +}) +$('#center_y_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个,对齐到背景垂直中心 + let obj = _objs[0] + obj.setCoords() + let coords = obj.getCoords(true) + let objMinY = getCoordsMinY(coords) + let objMaxY = getCoordsMaxY(coords) + let objCenterY = (objMinY + objMaxY) / 2 + let bgCoords = background_image.getCoords(true) + let bgMinY = getCoordsMinY(bgCoords) + let bgMaxY = getCoordsMaxY(bgCoords) + let bgCenterY = (bgMinY + bgMaxY) / 2 + let deltaY = bgCenterY - objCenterY + obj.set('top', obj.get('top') + deltaY) + obj.setCoords() + canvas.renderAll() + recordState() + return + } + // 多个对象对齐:找到所有对象边界框的中心Y值 + let minTop = Infinity + let maxBottom = -Infinity + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinY = getCoordsMinY(coords) + let itemMaxY = getCoordsMaxY(coords) + if (itemMinY < minTop) minTop = itemMinY + if (itemMaxY > maxBottom) maxBottom = itemMaxY + } + let groupCenterY = (minTop + maxBottom) / 2 + // 将所有对象对齐到中心Y值 + groupItems = [] + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinY = getCoordsMinY(coords) + let itemMaxY = getCoordsMaxY(coords) + let itemCenterY = (itemMinY + itemMaxY) / 2 + let deltaY = groupCenterY - itemCenterY + item.set('top', item.get('top') + deltaY) + item.setCoords() + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + recordState() +}) +$('#left_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个,对齐到背景左边缘 + let obj = _objs[0] + obj.setCoords() + let coords = obj.getCoords(true) + let objMinX = getCoordsMinX(coords) + let bgMinX = getCoordsMinX(background_image.getCoords(true)) + let deltaX = bgMinX - objMinX + obj.set('left', obj.get('left') + deltaX) + obj.setCoords() + canvas.renderAll() + recordState() + return + } + // 多个对象对齐:找到所有对象边界框的最小X值 + let minLeft = Infinity + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinX = getCoordsMinX(coords) + if (itemMinX < minLeft) { + minLeft = itemMinX + } + } + // 将所有对象对齐到最小X值 + groupItems = [] + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinX = getCoordsMinX(coords) + let deltaX = minLeft - itemMinX + item.set('left', item.get('left') + deltaX) + item.setCoords() + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + recordState() +}) +$('#right_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个,对齐到背景右边缘 + let obj = _objs[0] + obj.setCoords() + let coords = obj.getCoords(true) + let objMaxX = getCoordsMaxX(coords) + let bgMaxX = getCoordsMaxX(background_image.getCoords(true)) + let deltaX = bgMaxX - objMaxX + obj.set('left', obj.get('left') + deltaX) + obj.setCoords() + canvas.renderAll() + recordState() + return + } + // 多个对象对齐:找到所有对象边界框的最大X值 + let maxRight = -Infinity + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMaxX = getCoordsMaxX(coords) + if (itemMaxX > maxRight) { + maxRight = itemMaxX + } + } + // 将所有对象对齐到最大X值 + let groupItems = [] + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMaxX = getCoordsMaxX(coords) + let deltaX = maxRight - itemMaxX + item.set('left', item.get('left') + deltaX) + item.setCoords() + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + recordState() +}) +$('#center_x_alignment').click(function () { + let _objs = canvas.getActiveObjects() + if (_objs.length == 1) { + //只选中了一个,对齐到背景水平中心 + let obj = _objs[0] + obj.setCoords() + let coords = obj.getCoords(true) + let objMinX = getCoordsMinX(coords) + let objMaxX = getCoordsMaxX(coords) + let objCenterX = (objMinX + objMaxX) / 2 + let bgCoords = background_image.getCoords(true) + let bgMinX = getCoordsMinX(bgCoords) + let bgMaxX = getCoordsMaxX(bgCoords) + let bgCenterX = (bgMinX + bgMaxX) / 2 + let deltaX = bgCenterX - objCenterX + obj.set('left', obj.get('left') + deltaX) + obj.setCoords() + canvas.renderAll() + recordState() + return + } + // 多个对象对齐:找到所有对象边界框的中心X值 + let minLeft = Infinity + let maxRight = -Infinity + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinX = getCoordsMinX(coords) + let itemMaxX = getCoordsMaxX(coords) + if (itemMinX < minLeft) minLeft = itemMinX + if (itemMaxX > maxRight) maxRight = itemMaxX + } + let groupCenterX = (minLeft + maxRight) / 2 + // 将所有对象对齐到中心X值 + groupItems = [] + for (let item of _objs) { + item.setCoords() + let coords = item.getCoords(true) + let itemMinX = getCoordsMinX(coords) + let itemMaxX = getCoordsMaxX(coords) + let itemCenterX = (itemMinX + itemMaxX) / 2 + let deltaX = groupCenterX - itemCenterX + item.set('left', item.get('left') + deltaX) + item.setCoords() + groupItems.push(item) + } + canvas.discardActiveObject() + let sel = new fabric.ActiveSelection(groupItems, { + canvas: canvas + }) + canvas.setActiveObject(sel) + canvas.requestRenderAll() + canvas.renderAll() + recordState() +}) +$('#up_obj').click(function () { + //上移,放到前面 数组中在后面 + let idx = getIndex(canvas.getActiveObject()) + if (idx + 1 != canvas.getObjects().length) { + canvas.bringForward(canvas.getActiveObject()) + canvas.renderAll() + let temp = objs[idx - 1] + objs[idx - 1] = objs[idx] + objs[idx] = temp + //console.log(objs); + + recordState() + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx); + } + } +}) +$('#down_obj').click(function () { + //下移,放到后面 数组中在前面 + let idx = getIndex(canvas.getActiveObject()) + if (idx != 1) { + canvas.sendBackwards(canvas.getActiveObject()) + canvas.renderAll() + //[objs[idx-2],objs[idx-1]] = [objs[idx-1],objs[idx-2]]; + + let temp = objs[idx - 1] + objs[idx - 1] = objs[idx - 2] + objs[idx - 2] = temp + recordState() + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(idx - 2); + } + } +}) +$('#set_bg').click(function () { + //设置为背景 + let obj = canvas.getActiveObject() + let idx = getIndex(canvas.getActiveObject()) + const scaleX = 1012 / obj.width + const scaleY = 648 / obj.height + // 使用缩放后的实际尺寸计算位置,确保与初始背景图片位置一致 + const scaledWidth = obj.width * scaleX; + const scaledHeight = obj.height * scaleY; + obj.set({ + scaleX: scaleX, + scaleY: scaleY, + left: ($('#canvas1').width() - scaledWidth) / 2, + top: ($('#canvas1').height() - scaledHeight) / 2 + }) + $('#text_w').val(1012) + $('#text_h').val(648) + $('#text_x').val(0) + $('#text_y').val(0) + // 设为背景时,锁定所有编辑操作,并设置为不可选中 + obj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + $("#lock_img").attr("src", soonAsset("lock20.png")) + canvas.sendToBack(canvas.getActiveObject()) + canvas.bringForward(canvas.getActiveObject()) + canvas.renderAll() + let objItem = objs.splice(idx - 1, 1)[0] + objs.unshift(objItem) + + // 确保背景图(索引0)的锁定属性被正确设置 + setTimeout(() => { + let bgObj = canvas.getObjects()[0]; + if (bgObj) { + bgObj.set({ + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + selectable: false, + evented: false + }); + canvas.renderAll(); + } + }, 100); + + recordState() + if (typeof window.updateList === 'function') { + window.updateList(); + } + if (typeof window.selectObj === 'function') { + window.selectObj(0); + } +}) +$('#previous').click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + //撤销一次操作 + if (step.val == 0) { + return + } + + step.val-- + + if ($('#front_side').hasClass('ui-button-active')) { + // 使用 recordJson1 和 recordObjs1 + if (recordObjs1 && recordObjs1[step.val]) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + // 同步更新 window 变量 + window.objs1 = objs1; + window.objs = objs; + } + + if (recordJson1 && recordJson1[step.val]) { + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson1[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } + } else if ($('#back_side').hasClass('ui-button-active')) { + // 使用 recordJson2 和 recordObjs2 + if (recordObjs2 && recordObjs2[step.val]) { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + // 同步更新 window 变量 + window.objs2 = objs2; + window.objs = objs; + } + + if (recordJson2 && recordJson2[step.val]) { + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } + } +}) +$('#next').click(function () { + // 正在应用历史(撤销/重做)时,直接忽略新的点击,防止并发操作 + if (window._isApplyingHistory) { + return; + } + //撤销一次撤销操作 + // 根据当前是正面还是背面检查对应的 recordObjs 长度 + let maxLen; + if ($('#front_side').hasClass('ui-button-active')) { + maxLen = recordObjs1 ? recordObjs1.length : 0; + if (step.val >= maxLen - 1) { + return + } + } else if ($('#back_side').hasClass('ui-button-active')) { + maxLen = recordObjs2 ? recordObjs2.length : 0; + if (step.val >= maxLen - 1) { + return + } + } else { + return; + } + + ++step.val + + if ($('#front_side').hasClass('ui-button-active')) { + // 使用 recordJson1 和 recordObjs1 + if (recordObjs1 && recordObjs1[step.val]) { + objs1 = JSON.parse(recordObjs1[step.val]); + objs = objs1; + // 同步更新 window 变量 + window.objs1 = objs1; + window.objs = objs; + } + + if (recordJson1 && recordJson1[step.val]) { + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson1[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs1); + } + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } + } else if ($('#back_side').hasClass('ui-button-active')) { + // 使用 recordJson2 和 recordObjs2 + if (recordObjs2 && recordObjs2[step.val]) { + objs2 = JSON.parse(recordObjs2[step.val]); + objs = objs2; + // 同步更新 window 变量 + window.objs2 = objs2; + window.objs = objs; + } + + if (recordJson2 && recordJson2[step.val]) { + window._isApplyingHistory = true; + canvas.loadFromJSON(recordJson2[step.val], function () { + // 加载后解锁所有对象(背景除外) + if (typeof window.unlockAllObjects === 'function') { + window.unlockAllObjects(canvas); + } + // 重新生成二维码和条码(因为它们需要根据 objs 数据动态生成) + if (typeof window.regenerateQrCodesAndBarcodes === 'function') { + window.regenerateQrCodesAndBarcodes(canvas, objs2); + } + // 调用状态恢复函数 + if (typeof window.onStateRestored === 'function') { + window.onStateRestored(); + } + window._isApplyingHistory = false; + }); + } + } +}) +$('#clean').click(function () { + objs.splice(0, objs.length) + + let allObjs = canvas.getObjects() + for (let item of allObjs) { + if (item != allObjs[0]) { + canvas.remove(item) + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) +$('#rotate').click(function () { + let obj = canvas.getActiveObject() + obj.rotate(Math.round((obj.get('angle') + 90) % 360)) + $('#text_r').val(obj.get('angle')) + canvas.renderAll() + updateControls() + recordState() +}) +$('#vertical_obj').click(function () { + let obj = canvas.getActiveObject() + obj.set('flipY', !obj.get('flipY')) + canvas.renderAll() + updateControls() + recordState() +}) +$('#align_obj').click(function () { + let obj = canvas.getActiveObject() + obj.set('flipX', !obj.get('flipX')) + canvas.renderAll() + updateControls() + recordState() +}) +$('#text_italics').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('fontStyle', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontStyle', 'italic') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#text_bold').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选,恢复正常 + canvas.getActiveObject().set('fontWeight', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontWeight', 'bold') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#text_underline').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('underline', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('underline', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#text_linethrough').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('linethrough', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('linethrough', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$("#circle_text_italics").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选 + canvas.getActiveObject().set("fontStyle", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontStyle", "italic") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); +}); +$("#circle_text_bold").click(function () { + if ($(this).hasClass("ui-radio-acitve")) { + //已选,恢复正常 + canvas.getActiveObject().set("fontWeight", "normal") + $(this).removeClass("ui-radio-acitve") + } else { + //未选 + canvas.getActiveObject().set("fontWeight", "bold") + $(this).addClass("ui-radio-acitve") + } + recordState(); + canvas.renderAll(); +}); +$('#out_left_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_left_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'left') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'left' + recordState() + canvas.renderAll() +}) +$('#out_right_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_right_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'right') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'right' + recordState() + canvas.renderAll() +}) +$('#out_center_alignment').click(function () { + $('#out_left_alignment').parent().removeClass('tool1') + $('#out_right_alignment').parent().removeClass('tool1') + $('#out_center_alignment').parent().removeClass('tool1') + setTimeout(() => { + $('#out_center_alignment').parent().addClass('tool1') + }); + canvas.getActiveObject().set('textAlign', 'center') + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].align = 'center' + recordState() + canvas.renderAll() +}) +$('#out_text_italics').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('fontStyle', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontStyle', 'italic') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#out_text_bold').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选,恢复正常 + canvas.getActiveObject().set('fontWeight', 'normal') + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('fontWeight', 'bold') + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#out_text_underline').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('underline', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('underline', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#out_text_linethrough').click(function () { + if ($(this).hasClass('ui-radio-acitve')) { + //已选 + canvas.getActiveObject().set('linethrough', false) + $(this).removeClass('ui-radio-acitve') + } else { + //未选 + canvas.getActiveObject().set('linethrough', true) + $(this).addClass('ui-radio-acitve') + } + recordState() + canvas.renderAll() +}) +$('#lock').off('click').on('click', function (e) { + e.stopPropagation(); + if (!canvas.getActiveObject()) return; + let obj = canvas.getActiveObject(); + // 检查是否已锁定(通过lockMovementX判断,因为锁定时会设置所有lock属性) + let isLocked = obj.get('lockMovementX') === true; + let shouldLock = !isLocked; + + // 锁定/解锁所有编辑操作,但保持选中状态(不取消选中) + obj.set({ + lockMovementX: shouldLock, + lockMovementY: shouldLock, + lockRotation: shouldLock, + lockScalingX: shouldLock, + lockScalingY: shouldLock, + lockSkewingX: shouldLock, + lockSkewingY: shouldLock, + // 锁定时不可选中,解锁时可选中(但保持当前选中状态) + selectable: !shouldLock, + evented: !shouldLock + }); + if (shouldLock) { + $('#lock_img').attr('src', soonAsset('lock20.png')) //已锁定 + } else { + $('#lock_img').attr('src', soonAsset('unlock20.png')) //已解锁 + } + // 更新控件(保持选中状态,所以可以更新) + updateControls(); + recordState() + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas.renderAll(); +}) +$('#text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#text_back_color').val('#ffffff') + canvas.renderAll() +}) +$('#circle_text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#circle_text_back_color').val('#ffffff') + canvas.renderAll() +}) +$('#out_text_back_transparent').click(function () { + canvas.getActiveObject().set('textBackgroundColor', '') + $('#out_text_back_color').val('#ffffff') + canvas.renderAll() +}) +$('#rect_transparent').click(function () { + canvas.getActiveObject().set('fill', '') + $('#rect_color').val('#ffffff') + canvas.renderAll() +}) +$('#rect_stroke_transparent').click(function () { + canvas.getActiveObject().set('stroke', '') + $('#rect_stroke').val('#ffffff') + canvas.renderAll() +}) +$('#circle_transparent').click(function () { + canvas.getActiveObject().set('fill', '') + $('#circle_color').val('#ffffff') + canvas.renderAll() +}) +$('#circle_stroke_transparent').click(function () { + canvas.getActiveObject().set('stroke', '') + $('#circle_stroke').val('#ffffff') + canvas.renderAll() +}) +// 清理不需要的 src 字段(保留自定义图片的 src) +// 复制 +$("#copy").click(function () { + window.copySelection(); +}) +// 粘贴 +$("#paste").click(function () { + window.pasteFromClipboard(); +}) +$('#save').click(function () { + if (typeof window.output === 'function') { + window.output(); + } +}); +function getDate() { + let date = new Date() + return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() +} +function getAbsoluteXY(_obj) { + let coord = _obj.get('lineCoords') + //let xs = [], ys = []; + return [_obj.get('left'), _obj.get('top')] +} +$('#open').click(function () { + if (step.val == 0) { + OpenDialog(); + return; + } + layer.confirm( + '' + language_str('whetherSave') + '', + function (index) { + if (typeof window.output === 'function') { + window.output(OpenDialog); + } + layer.close(index); + }, + function (index) { + OpenDialog(); + } + ) + function OpenDialog() { + if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return; + var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null); + if (!dialogApi) return; + dialogApi.showOpenDialog({ title: '请选择文件', buttonLabel: language_str('comf'), filters: [{ name: 'Soon File Type', extensions: ['soon'] }] }) + .then(function(result) { + if (result.canceled) return; + var file = (result.files && result.files[0]) || result.file; + function openWithKey(fileKey, j) { + var type = j.soonType ? j.soonType : (j.backBlackPic ? 2 : 1); + if (type == 1 && window.platformBridge && window.platformBridge.openDesignPage) { + window.platformBridge.openDesignPage(fileKey, 1); + return; + } + if (typeof window.openFile === 'function') window.openFile(fileKey, j); + } + function importAndOpen(j, fileName) { + if (!window.platformBridge || !window.platformBridge.importSoonFile) return; + window.platformBridge.importSoonFile(fileName || 'design.soon', j).then(function(res) { + if (res && res.fileKey) openWithKey(res.fileKey, j); + }).catch(function () {}); + } + if (file && file.text) { + file.text().then(function(t) { + importAndOpen(JSON.parse(t), file.name || 'design.soon'); + }).catch(function() {}); + return; + } + var filePath = result.filePaths && result.filePaths[0]; + if (!filePath) return; + var fs = typeof require !== 'undefined' ? require('fs') : null; + if (!fs) return; + importAndOpen(JSON.parse(fs.readFileSync(filePath).toString()), filePath); + }) + .catch(function(err) {}); + } +}) +function goFirstPage() { + if (window.platformBridge && window.platformBridge.openFirstPage) window.platformBridge.openFirstPage(); + else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-first-page'); +} +$('#new').click(function () { + if (step.val == 0) { + goFirstPage(); + return; + } + layer.confirm( + '' + language_str('whetherSave') + '', + function (index) { + if (typeof window.output === 'function') { + window.output(goFirstPage); + } + layer.close(index); + }, + function (index) { + goFirstPage(); + } + ) +}) +function chagneTitle() { } +function addBackground() { + // 类型改变 + fabric.Image.fromURL(soonAsset('bg_front_2.png'), function (image) { + //image.set("scaleX",$("#canvas-div").width() / 1200 < 1 ? $("#canvas-div").width() / 1200 : 1) + + image.left = ($('#canvas1').width() - image.width * image.get('scaleX')) / 2 + image.top = ($('#canvas1').height() - image.height * image.get('scaleY')) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + image.setSrc(image.toDataURL(), function (image) { + background_image1 = image + background_image = background_image1 + canvas.add(image) + canvas.renderAll() + //pre_objs1.push(JSON.stringify(objs1)); + + recordObjs1.push(JSON.stringify(objs1)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + recordJson1.push(canvas1.toJSON(props)) + }) + // 类型改变 + fabric.Image.fromURL(soonAsset('bg_back_2.png'), function (image) { + image.left = ($('#canvas2').width() - image.width) / 2 + image.top = ($('#canvas2').height() - image.height) / 2 + + image.selectable = false + image.hoverable = false + image.hoverCursor = 'default' + + image.setSrc(image.toDataURL(), function (image) { + background_image2 = image + canvas2.add(image) + canvas2.renderAll() + //pre_objs2.push(JSON.stringify(objs2)); + + recordObjs2.push(JSON.stringify(objs2)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped']; + recordJson2.push(canvas2.toJSON(props)) + + let file = GetFile().get('file') + if (file && file != 'empty') { + if (typeof window.openFile === 'function') { + window.openFile(file); + } + } + }) + }) + is_bgi_add = true + }) +} +function selectObject(target, index) { + updateControls() + + $('#base_control').show() + //$("#text_x").val((target.left - background_image.left).toFixed(1)); +} +function updateControls() { + // 检查是否有选中对象,如果没有则直接返回 + if (!canvas || !canvas.getActiveObject()) return; + + const activeObj = canvas.getActiveObject(); + $('#text_r').val(activeObj.get('angle').toFixed(0)) + $('#text_x').val((activeObj.get('left') - background_image.left).toFixed(0)) + $('#text_y').val((activeObj.get('top') - background_image.top).toFixed(0)) + $('#text_h').val((activeObj.get('scaleY') * activeObj.get('height')).toFixed(0)) + $('#text_w').val((activeObj.get('scaleX') * activeObj.get('width')).toFixed(0)) + // 根据锁定状态显示图标(使用lockMovementX判断,因为锁定时会设置所有lock属性) + if (activeObj.get('lockMovementX')) { + $('#lock_img').attr('src', soonAsset('lock20.png')) //已锁定 + } else { + $('#lock_img').attr('src', soonAsset('unlock20.png')) //未锁定 + } +} +function checkName(name) { + //检查字段名是否重复了 + for (let item of objs1) { + if (item.name == name) { + return false + } + } + for (let item of objs2) { + if (item.name == name) { + return false + } + } + return true +} +function resName(pre_name) { + //返回的是不重复的字段名 + let num = objs1.length + objs2.length - 1 + do { + num++ + } while (!checkName(pre_name + num)) //重复了就再加1 + return pre_name + num +} +function addPic(pointer) { + pre_add_image.left = pointer.x + pre_add_image.top = pointer.y + + canvas.add(pre_add_image) + + objs.push({ type: 1, black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() + //OpenDialog(); +} +function selectPic(target, index) { + // 检查是否有选中对象,如果没有则直接返回 + if (!canvas || !canvas.getActiveObject()) return; + + let activeObj = canvas.getActiveObject(); + + // 检查对象是否被锁定 + if (isObjectLocked(activeObj)) { + canvas.discardActiveObject(); + canvas.renderAll(); + return; + } + + //透明度 + $('#pic_control').show() + $("#set_bg").show(); + + // 检查 pic_op 是否存在且有 setValue 方法 + if (typeof pic_op !== 'undefined' && pic_op && typeof pic_op.setValue === 'function') { + pic_op.setValue(100 - activeObj.get('opacity') * 100); + } else { + $("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%"); + $("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%"); + } + + // $("#pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function addOutPic(pointer) { + fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) { + let width = canvas.getWidth() + let height = canvas.getHeight() + image.left = pointer.x + image.top = pointer.y + image.opacity = 1 + //image.on("selected",function(){//test well + image.setSrc(image.toDataURL(), function (image) { + canvas.add(image) + objs.push({ type: 2, name: resName('OUT_PIC_'), black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() + }) + }) +} +function selectOutPic(target, index) { + //字段名 + $('#out_pic_control').show() + out_pic_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + let idx = getIndex(canvas.getActiveObject()) + $('#out_pic_field_name').val(objs[idx - 1].name) + // $("#out_pic_black").prop("checked", objs[idx-1].black ? 'checked' : ''); + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} + +function addText(pointer) { + // 设置默认字号为30px(像素值),这样显示的字号约为 30 * 72 / 300 = 7.2pt + let text = new fabric.IText('TEXT', { + //源:文本 + left: pointer.x, + top: pointer.y, + fill: '#000000', + textBackgroundColor: '', + fontStyle: 'normal', + fontWeight: 'normal', + fontSize: 30, + underline: false, + linethrough: false, + fontFamily: 'Arial' + }) + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + canvas.add(text) + objs.push({ type: 3, black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +} +function selectText() { + //console.log(canvas.getActiveObject().get("fontFamily")); + $('#text_control').show() + $('#text_color').val(canvas.getActiveObject().get('fill')) + $('#text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#text_size").val(val); + $('#text_text').val(canvas.getActiveObject().get('text')) + $('#text_font_family').val(canvas.getActiveObject().get('fontFamily')) + + if (canvas.getActiveObject().get('underline') == true) { + $('#text_underline').addClass('ui-radio-acitve') + } else { + $('#text_underline').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontWeight') == 'bold') { + $('#text_bold').addClass('ui-radio-acitve') + } else { + $('#text_bold').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontStyle') == 'italic') { + $('#text_italics').addClass('ui-radio-acitve') + } else { + $('#text_italics').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('linethrough') == true) { + $('#text_linethrough').addClass('ui-radio-acitve') + } else { + $('#text_linethrough').removeClass('ui-radio-acitve') + } + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function getDisdance(x1, y1, x2, y2) { + var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值 + var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值 + var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离 + return distance; +} +function getDeg(pointer) { + // 计算点击位置与画布中心的坐标差 + var centerX = canvas.width / 2; + var centerY = canvas.height / 2; + var mouseX = pointer.x; + var mouseY = pointer.y; + var dx = mouseX - centerX; + var dy = mouseY - centerY; + // 计算旋转角度(弧度) + var angleRad = Math.atan2(dy, dx); + // 将角度转换为度数 + var angleDeg = angleRad * (180 / Math.PI); + if (angleDeg < 0) { + angleDeg += 360; + } + angleDeg += 90 + return angleDeg +} +function addCircleText(pointer) { + let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2) + let angleDeg = getDeg(pointer) + let text = new fabric.CurvedText('Circle TEXT', {//源:圆形文本 + left: pointer.x, + top: pointer.y, + fill: "#000000", + textBackgroundColor: "", + fontStyle: "normal", + fontWeight: "normal", + diameter: distance * 2, + fontSize: 30, + underline: false, + linethrough: false, + lockScalingX: true, + lockScalingY: true, + fontFamily: 'Arial', + originX: 'center', + originY: 'center', + angle: angleDeg, + flipped: pointer.y > canvas.height / 2 ? true : false + }); + // distance = getDisdance(pointer.x+text.width/2, pointer.y, canvas.width/2,canvas.height/2) + text.setControlVisible("ml", false); + text.setControlVisible("mt", false); + text.setControlVisible("mr", false); + text.setControlVisible("mb", false); + canvas.add(text); + objs.push({ type: 11, black: false, circleCenter: 0 }); + canvas.renderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + }; + recordState(); +} +function selectCircleText() { + let idx = getIndex(canvas.getActiveObject()); + $("#circle_text_control").show(); + $("#circle_text_color").val(canvas.getActiveObject().get("fill")); + $("#circle_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor")); + if (typeof circle_text_size_updating !== 'undefined') { + circle_text_size_updating = true; + } + circle_text_size.setValue(canvas.getActiveObject().get("fontSize")); + if (typeof circle_text_size_updating !== 'undefined') { + circle_text_size_updating = false; + } + circle_text_diameter.setValue(canvas.getActiveObject().get("diameter")); + $("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : ''); + $("#circle_text_text").val(canvas.getActiveObject().get("text")); + $("#circle_text_font_family").val(canvas.getActiveObject().get("fontFamily")); + + if (canvas.getActiveObject().get("underline") == true) { + $("#circle_text_underline").addClass("ui-radio-acitve") + } else { + $("#circle_text_underline").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontWeight") == "bold") { + $("#circle_text_bold").addClass("ui-radio-acitve") + } else { + $("#circle_text_bold").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("fontStyle") == "italic") { + $("#circle_text_italics").addClass("ui-radio-acitve") + } else { + $("#circle_text_italics").removeClass("ui-radio-acitve") + } + if (canvas.getActiveObject().get("linethrough") == true) { + $("#circle_text_linethrough").addClass("ui-radio-acitve") + } else { + $("#circle_text_linethrough").removeClass("ui-radio-acitve") + } + $("#pic_control").hide(); + $("#set_bg").hide(); + $("#rotate").hide(); + $("#out_pic_control").hide(); + $("#text_control").hide(); + $("#out_text_control").hide(); + $("#rect_control").hide(); + $("#circle_control").hide(); + $("#line_control").hide(); + $("#counter_control").hide(); + $("#qrcode_Controll").hide(); + $("#barcode_controll").hide(); + +} +function addOutText(pointer) { + //MERGE_TEXT_1 + let text = new fabric.Text('MERGE TEXT', { + //源:合成文本 + left: pointer.x, + top: pointer.y, + fill: '#000000', + fontStyle: 'normal', + fontWeight: 'normal', + textBackgroundColor: '', + underline: false, + linethrough: false, + fontFamily: 'Arial', + textAlign: 'center' + }) + + //text.on({ + text.setControlVisible('ml', false) + text.setControlVisible('mt', false) + text.setControlVisible('mr', false) + text.setControlVisible('mb', false) + + canvas.add(text) + objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +} +function selectOutText() { + //color + const align = canvas.getActiveObject().get('textAlign') + if (align === 'left') { + $('#out_left_alignment').parent().addClass('tool1') + } else if (align === 'right') { + $('#out_right_alignment').parent().addClass('tool1') + } else { + $('#out_center_alignment').parent().addClass('tool1') + } + $('#out_text_control').show() + $('#out_text_color').val(canvas.getActiveObject().get('fill')) + $('#out_text_text').val(canvas.getActiveObject().get('text')) + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#out_text_size").val(val); + $('#out_text_back_color').val(canvas.getActiveObject().get('textBackgroundColor') == '' ? '#ffffff' : canvas.getActiveObject().get('textBackgroundColor')) + let idx = getIndex(canvas.getActiveObject()) + $('#out_text_field_name').val(objs[idx - 1].name) + $('#out_text_font_family').val(canvas.getActiveObject().get('fontFamily')) + $('#exceed').prop('checked', objs[idx - 1].exceed ? 'checked' : '') + $('#autoWrap').prop('checked', objs[idx - 1].autoWrap ? 'checked' : '') + $('#fixSize').prop('checked', objs[idx - 1].fixSize ? 'checked' : '') + if (canvas.getActiveObject().get('underline') == true) { + $('#out_text_underline').addClass('ui-radio-acitve') + } else { + $('#out_text_underline').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontWeight') == 'bold') { + $('#out_text_bold').addClass('ui-radio-acitve') + } else { + $('#out_text_bold').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('fontStyle') == 'italic') { + $('#out_text_italics').addClass('ui-radio-acitve') + } else { + $('#out_text_italics').removeClass('ui-radio-acitve') + } + if (canvas.getActiveObject().get('linethrough') == true) { + $('#out_text_linethrough').addClass('ui-radio-acitve') + } else { + $('#out_text_linethrough').removeClass('ui-radio-acitve') + } + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#rect_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function addRect(pointer) { + let rect = new fabric.Rect({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: "#ff0000", + strokeWidth: 3, + width: 100, + height: 100 + }) + canvas.add(rect) + objs.push({ type: 5, black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +} +function selectRect() { + //color + $('#rect_control').show() + $('#rect_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) + $('#rect_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) + $('#rect_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) + $('#rect_radius').val(canvas.getActiveObject().get('rx')) + rect_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + let a = canvas.getActiveObject().getCoords() + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#circle_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function addCircle(pointer) { + let circle = new fabric.Circle({ + left: pointer.x, + top: pointer.y, + fill: '', + stroke: '#ff0000', + strokeWidth: 3, + radius: 100, + strokeUniform: true + }) + canvas.add(circle) + objs.push({ type: 6, black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +} +function selectCircle() { + //border-color + $('#circle_control').show() + $('#circle_color').val(canvas.getActiveObject().get('fill') == '' ? '#ffffff' : canvas.getActiveObject().get('fill')) + $('#circle_stroke').val(canvas.getActiveObject().get('stroke') == '' ? '#ffffff' : canvas.getActiveObject().get('stroke')) + $('#circle_stroke_width').val(canvas.getActiveObject().get('strokeWidth')) + circle_op.setValue(100 - canvas.getActiveObject().get('opacity') * 100) + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#line_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function addLine(pointer) { + let line = new fabric.Line([50, 100, 200, 100], { + left: pointer.x, + top: pointer.y, + stroke: '#ff0000', + strokeWidth: 3, + strokeDashArray: [0, 0], + }) + + line.setControlVisible('tl', false) + line.setControlVisible('tr', false) + line.setControlVisible('br', false) + line.setControlVisible('bl', false) + line.setControlVisible('mt', false) + line.setControlVisible('mb', false) + canvas.add(line) + objs.push({ type: 7, black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +} +function selectLine() { + //color + $('#line_control').show() + + let idx = getIndex(canvas.getActiveObject()) + $('#line_color').val(canvas.getActiveObject().get('stroke')) + $('#line_dist').val(canvas.getActiveObject().get('strokeDashArray')[0]) + $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) + + $('#pic_control').hide() + $("#set_bg").hide(); + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function addQrCode(pointer) { + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 }) + fabric.Image.fromURL(qr, function (image) { + image.left = pointer.x + image.top = pointer.y + canvas.add(image) + objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() + }) +} +function selectQrCode() { + //字段名 + $('#qrcode_Controll').show() + let idx = getIndex(canvas.getActiveObject()) + $('#qrcode_field_name').val(objs[idx - 1].name) + $('#qrcode_color').val(objs[idx - 1].color) + $('#qrcode_val').val(objs[idx - 1].val) + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#barcode_controll').hide() +} +function addBarCode(pointer) { + JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + fabric.Image.fromURL(bar, function (image) { + image.left = pointer.x + image.top = pointer.y + + canvas.add(image) + objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false }) //show是取消文字显示 + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() + }) +} +function selectBarCode() { + //字段名 + $('#barcode_controll').show() + let idx = getIndex(canvas.getActiveObject()) + $('#barcode_field_name').val(objs[idx - 1].name) + $('#barcode_font_family').val(objs[idx - 1].font) + let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#barcode_size").val(val); + $('#barcode_val').val(objs[idx - 1].val) + $('#barcode_show').prop('checked', objs[idx - 1].show ? 'checked' : '') + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#counter_control').hide() + $('#qrcode_Controll').hide() +} +function addCounter(pointer) { + let text = new fabric.Text('0', { + left: pointer.x, + top: pointer.y, + fill: '#000000' + }) + canvas.add(text) + objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false }) + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +} +function selectCounter() { + //起始 + $('#counter_control').show() + + let idx = getIndex(canvas.getActiveObject()) + $('#counter_start').val(objs[idx - 1].start) + $('#counter_add').val(objs[idx - 1].add) + $('#counter_color').val(canvas.getActiveObject().get('fill')) + $('#count_font_family').val(canvas.getActiveObject().get('fontFamily')) + let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi + if (val % 0.5 !== 0) val = Math.round(val * 2) / 2 + $("#count_size").val(val); + + $('#line_control').hide() + $('#pic_control').hide() + $('#out_pic_control').hide() + $('#text_control').hide() + $("#circle_text_control").hide(); + $('#out_text_control').hide() + $('#rect_control').hide() + $('#circle_control').hide() + $('#qrcode_Controll').hide() + $('#barcode_controll').hide() +} +function getIndex(target, _canvas = null) { + let temp_objs + if (_canvas == null) { + temp_objs = canvas.getObjects() + } else { + temp_objs = _canvas.getObjects() + } + + let i = 0 + for (let res of temp_objs) { + if (target == res) { + return i + } + i++ + } + return 0 +} +function getType(target) { + let temp_objs = canvas.getObjects() + let i = 0 + for (let res of temp_objs) { + if (target == res) { + return objs[i].type + } + i++ + } +} + +// updateList 和 selectObj 函数在 core.js 中定义并附加到 window 对象 +function recordState() { + //pre_objs.push(JSON.stringify(objs)); + ++step.val + for (let i = step.val; i < recordObjs.length; i++) { + recordObjs.pop() + recordJson.pop() + } + //recordObjs.splice(step.val, 1, JSON.stringify(objs)) + recordObjs.push(JSON.stringify(objs)) + const props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY', 'lockSkewingX', 'lockSkewingY', 'evented']; + j = canvas.toJSON(props) + j.objects[0].hoverCursor = 'default' + recordJson.push(j) +} +function getCoordsMinX(acoords) { + x = acoords[0].x + for (let item of acoords) { + if (item.x < x) { + x = item.x + } + } + return x +} +function getCoordsMaxX(acoords) { + x = acoords[0].x + for (let item of acoords) { + if (item.x > x) { + x = item.x + } + } + return x +} +function getCoordsMinY(acoords) { + y = acoords[0].y + for (let item of acoords) { + if (item.y < y) { + y = item.y + } + } + return y +} +function getCoordsMaxY(acoords) { + y = acoords[0].y + for (let item of acoords) { + if (item.y > y) { + y = item.y + } + } + return y +} + +$('#text_y').bind('input propertychange', function () { + if ($('#text_y').val() == '') { + $('#text_y').val(0) + } + let val = background_image.top + if ($('#text_y').val() != '') { + val = background_image.top + parseInt($('#text_y').val()) + } + canvas.getActiveObject().set('top', val) + canvas.renderAll() + recordState() +}) +$('#text_x').bind('input propertychange', function () { + if ($('#text_x').val() == '') { + $('#text_x').val(0) + } + let val = background_image.left + if ($('#text_x').val() != '') { + val = background_image.left + parseInt($('#text_x').val()) + } + canvas.getActiveObject().set('left', val) + canvas.renderAll() + recordState() +}) +$('#text_w').bind('input propertychange', function () { + let val = '0' + if ($('#text_w').val() != '') { + val = $('#text_w').val() + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type + if (type == 3 || type == 4) { + canvas.getActiveObject().set('width', parseInt(val)) + } else { + canvas.getActiveObject().set('scaleX', parseInt(val) / canvas.getActiveObject().get('width')) + } + + canvas.renderAll() + recordState() +}) +$('#text_h').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + if (objs[idx - 1].type == 7) { + if ($('#text_h').val() == '') { + $('#text_h').val(canvas.getActiveObject().get('strokeWidth')) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#text_h').val())) + canvas.renderAll() + return + } + let val = '0' + if ($('#text_h').val() != '') { + val = $('#text_h').val() + } + let type = objs[getIndex(canvas.getActiveObject()) - 1].type + if (type == 3 || type == 4) { + canvas.getActiveObject().set('height', parseInt(val)) + } else { + canvas.getActiveObject().set('scaleY', parseInt(val) / canvas.getActiveObject().get('height')) + } + + canvas.renderAll() + recordState() +}) +$('#text_r').bind('input propertychange', function () { + if ($('#text_r').val() == '') { + $('#text_r').val('0') + } + $('#text_r').val(parseInt($('#text_r').val()) % 360) + //canvas.getActiveObject().set("rotate", val) + canvas.getActiveObject().rotate(parseInt($('#text_r').val())) + canvas.renderAll() + recordState() +}) +$('#out_pic_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#out_pic_field_name').val() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) + +$('#text_black').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].black = true + } else { + objs[idx - 1].black = false + } + recordState() +}) +// $("#out_pic_black").click(function () { +$('#out_text_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#out_text_field_name').val() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) +$('#qrcode_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#qrcode_field_name').val() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) +$('#qrcode_color').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0, foreground: $(this).val() }) + let idx = getIndex(item) + objs[idx - 1].color = $(this).val() + item.setSrc(qr, function (img) { + canvas.renderAll() + recordState() + }) +}) +$("#qrcode_val").bind('input propertychange', function () { + let val = $(this).val() ? $(this).val() : 'https://www.cardsoon.com' + let item = canvas.getActiveObject(); + let qr = jrQrcode.getQrBase64(val, { padding: 0, foreground: $("#qrcode_color").val() }); + let idx = getIndex(item); + objs[idx - 1].val = val; + item.setSrc(qr, function (img) { + canvas.renderAll(); + recordState(); + }); +}); +$('#barcode_color').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].color = $(this).val() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) +}) +$('#barcode_field_name').bind('input propertychange', function () { + let idx = getIndex(canvas.getActiveObject()) + objs[idx - 1].name = $('#barcode_field_name').val() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) +$('#text_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#text_color').val()) + canvas.renderAll() + recordState() +}) +$('#text_back_color').bind('input propertychange', function () { + canvas.getActiveObject().set('textBackgroundColor', $('#text_back_color').val()) + canvas.renderAll() + recordState() +}) +$('#out_text_back_color').bind('input propertychange', function () { + canvas.getActiveObject().set('textBackgroundColor', $('#out_text_back_color').val()) + + canvas.renderAll() + recordState() +}) +$("#text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#text_size").val())) { + val = parseFloat($("#text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#text_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); +}) +$("#circle_text_color").bind('input propertychange', function () { + canvas.getActiveObject().set("fill", $("#circle_text_color").val()); + canvas.renderAll(); + recordState(); +}) +$("#circle_text_back_color").bind('input propertychange', function () { + canvas.getActiveObject().set("textBackgroundColor", $("#circle_text_back_color").val()); + + canvas.renderAll(); + recordState(); +}) +$("#circle_text_text").bind('input propertychange', function () { + let val = $("#circle_text_text").val().trim() + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); + canvas.renderAll(); + if (typeof window.updateList === 'function') { + window.updateList(); + }; + recordState(); +}) +$("#circle_text_center").click(function () { + let idx = getIndex(canvas.getActiveObject()); + if ($(this).prop("checked")) { + objs[idx - 1].circleCenter = 1; + } else { + objs[idx - 1].circleCenter = 0; + } + recordState(); +}); +$('#out_text_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#out_text_color').val()) + canvas.renderAll() + recordState() +}) +$("#out_text_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#out_text_size").val())) { + val = parseFloat($("#out_text_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#out_text_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); +}) +$('#out_text_text').bind('input propertychange', function () { + let val = $("#out_text_text").val().trim() + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) +$('#rect_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#rect_color').val()) + + canvas.renderAll() + recordState() +}) +$('#rect_stroke').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#rect_stroke').val()) + + canvas.renderAll() + recordState() +}) +$('#rect_stroke_width').bind('input propertychange', function () { + if ($('#rect_stroke_width').val() == '') { + $('#rect_stroke_width').val('0') + } else { + $('#rect_stroke_width').val(parseInt($('#rect_stroke_width').val())) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#rect_stroke_width').val())) + canvas.renderAll() + recordState() +}) +$('#rect_radius').bind('input propertychange', function () { + if ($('#rect_radius').val() == '') { + $('#rect_radius').val('0') + } else { + $('#rect_radius').val(parseInt($('#rect_radius').val())) + } + canvas.getActiveObject().set('rx', parseInt($('#rect_radius').val())) + canvas.getActiveObject().set('ry', parseInt($('#rect_radius').val())) + canvas.renderAll() + recordState() +}) +$('#circle_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#circle_color').val()) + + canvas.renderAll() + recordState() +}) +$('#circle_stroke').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#circle_stroke').val()) + + canvas.renderAll() + recordState() +}) +$('#circle_stroke_width').bind('input propertychange', function () { + if ($('#circle_stroke_width').val() == '') { + $('#circle_stroke_width').val('0') + } else { + $(this).val(parseInt($(this).val())) + } + canvas.getActiveObject().set('strokeWidth', parseInt($('#circle_stroke_width').val())) + canvas.renderAll() + recordState() +}) +$('#counter_start').bind('input propertychange', function () { + canvas.getActiveObject().set('text', $('#counter_start').val()) + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].start = $(this).val() + canvas.renderAll() + recordState() +}) +$('#counter_add').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].add = $(this).val() + canvas.renderAll() + recordState() +}) +$('#counter_color').bind('input propertychange', function () { + canvas.getActiveObject().set('fill', $('#counter_color').val()) + canvas.renderAll() + recordState() +}) +$("#count_size").bind('input propertychange', function () { + let val = 1 + if (parseFloat($("#count_size").val())) { + val = parseFloat($("#count_size").val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#count_size").val(val) + canvas.getActiveObject().set("fontSize", val * dpi / 72); + $("#text_h").val((canvas.getActiveObject().get("height")).toFixed(0)); + $("#text_w").val((canvas.getActiveObject().get("width")).toFixed(0)); + canvas.renderAll(); + recordState(); +}) +$('#line_color').bind('input propertychange', function () { + canvas.getActiveObject().set('stroke', $('#line_color').val()) + canvas.renderAll() + recordState() +}) +$('#line_dist').bind('input propertychange', function () { + if ($('#line_dist').val() == '') { + $('#line_dist').val(0) + } + canvas.getActiveObject().set('strokeDashArray', [parseInt($('#line_dist').val()), parseInt($('#line_dist').val())]) + canvas.renderAll() + recordState() +}) +$('#text_text').bind('input propertychange', function () { + let val = $("#text_text").val().trim() + // 有内容时不加首行缩进(前面的空格),只保留后面的空格 + canvas.getActiveObject().set("text", val ? `${val} ` : ` ${val} `); + canvas.renderAll() + if (typeof window.updateList === 'function') { + window.updateList(); + } + recordState() +}) +$('#barcode_val').bind('input propertychange', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].val = $(this).val() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) +}) +$("#barcode_show").click(function () { + let item = canvas.getActiveObject(); + let idx = getIndex(canvas.getActiveObject()); + let val = $("#barcode_size").val(); + let displayValue = true; + if ($(this).prop("checked")) { + objs[idx - 1].show = true; + displayValue = false; + } else { + objs[idx - 1].show = false; + displayValue = true; + } + // val = 0; // Remove this hack + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: val * dpi / 72, font: objs[idx - 1].font, displayValue: displayValue }); + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); +}); +$("#barcode_size").bind('input propertychange', function () { + let item = canvas.getActiveObject(); + let idx = getIndex(item); + let val = 1 + if (parseFloat($(this).val())) { + val = parseFloat($(this).val()) + if (val < 1) { + val = 1 + } else if (val > dpi) { + val = dpi + } else if (val % 0.5 !== 0) { + val = Math.round(val * 2) / 2 + } + + } + $("#barcode_size").val(val) + objs[idx - 1].fontSize = val * dpi / 72; + JsBarcode("#barcode", objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }); + + let barcode = document.getElementById('barcode'); + let bar = barcode.toDataURL("image/png"); + item.setSrc(bar, function (img) { + canvas.renderAll(); + recordState(); + }); +}) +$('#count_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() +}) +$('#text_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() +}) + +$('#circle_text_font_family').on('change', function () { + canvas.getActiveObject().set("fontFamily", $(this).find('option:selected').text()) + canvas.renderAll() + recordState(); +}); + +$('#language_select').on('change', function () { + //语言选择 + langua_ge($(this).find('option:selected').val()) + s_lan = $(this).find('option:selected').val() + localStorage.setItem('lang', s_lan) +}) +$('#barcode_font_family').on('change', function () { + let item = canvas.getActiveObject() + let idx = getIndex(item) + objs[idx - 1].font = $(this).find('option:selected').text() + JsBarcode('#barcode', objs[idx - 1].val, { margin: 0, lineColor: objs[idx - 1].color, fontSize: objs[idx - 1].fontSize, font: objs[idx - 1].font }) + + let barcode = document.getElementById('barcode') + let bar = barcode.toDataURL('image/png') + item.setSrc(bar, function (img) { + canvas.renderAll() + recordState() + }) + //recordState(); +}) +$('#out_text_font_family').on('change', function () { + canvas.getActiveObject().set('fontFamily', $(this).find('option:selected').text()) + canvas.renderAll() + recordState() +}) +$('.obj_list').on('click', 'div', function () { + let idx = objs.length - 1 - $(this).index() + selectObj(idx) +}) +$('#exceed').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].exceed = true + } else { + objs[idx - 1].exceed = false + } + recordState() +}) +$('#autoWrap').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].autoWrap = true + } else { + objs[idx - 1].autoWrap = false + } + recordState() +}) +$('#fixSize').click(function () { + let idx = getIndex(canvas.getActiveObject()) + if ($(this).prop('checked')) { + objs[idx - 1].fixSize = true + } else { + objs[idx - 1].fixSize = false + } + recordState() +}) +$('#delete').click(function () { + let actObjs = canvas.getActiveObjects() + let allObjs = canvas.getObjects() + let flag = 0 + for (let item of actObjs) { + if (item != allObjs[0]) { + let i = getIndex(item) //0是背景,所以从1开始,但objs不包含背景图片,故要-1 + objs.splice(i - 1, 1) + canvas.remove(item) + flag = 1 + } + } + if (typeof window.updateList === 'function') { + window.updateList(); + } + canvas.discardActiveObject() + if (flag == 1) { + recordState() + canvas.renderAll() + } + // let actObjs = canvas.getActiveObjects(); +}) + +//在关于我们获取版本号。 +$("#about").click(async function () { + + // 1. 先异步获取版本号 + let version = "0.0.0"; // 默认值,防止获取失败 + try { + if (window.sysAPI && window.sysAPI.getAppVersion) { + const v = await window.sysAPI.getAppVersion(); + version = "v" + v; // 拼接一个 v,变成 v2.3.017 + } + } catch (e) { + } + + // 2. 获取成功后再弹出窗口 + layer.open({ + type: 1 + , title: language_str("about") + , area: '450px;' + , id: 'LAY_layuipro' + , moveType: 1 + , content: '
    ' + + '
    ' + + '
    Soon Design
    ' + + // 【修改点】这里使用了刚才获取到的 version 变量 + '
    ' + language_str("vers") + ' ' + version + '
    ' + + '
    ' + language_str("copyright") + '
    ' + + '
    ' + }); +}) +$("#help").click(function() { + if (window.platformBridge && window.platformBridge.openHelp) window.platformBridge.openHelp(); + else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-help-file'); +}); + +// =========================================================== +$('#display').off('click').on('click', function () { + if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return; + // 类型改变 + fabric.Image.fromURL(soonAsset('front_bg') + bg_version + '_2.png', function (i1) { + i1.left = background_image.left + i1.top = background_image.top + let img1 = i1 + + fabric.Image.fromURL(soonAsset('back_bg_2.png'), function (i2) { + i2.left = background_image.left + i2.top = background_image.top + let img2 = i2 + fabric.Image.fromURL(soonAsset('op_2.png'), function (i3) { + i2.left = background_image.left + i2.top = background_image.top + let img3 = i3 + if (typeof window.display_func === 'function') { + window.display_func(img1, img2, img3); + } + }) + }) + }) +}); +$("#output").click(function () { + // 另存为:传递 saveAs 函数作为第二个参数 + if (typeof window.output === 'function') { + window.output(null, saveAs); // 调用output函数,使用saveAs进行另存为 + } +}); + +// 防止重复初始化事件监听器 +if (typeof window._design2CanvasEventsInitialized === 'undefined') { + window._design2CanvasEventsInitialized = true; + + // 所有 canvas 事件已在 core.js 的 initCanvasEvents() 中统一处理,这里不再重复监听 + // 如果需要 canvas1 特定的 mouse:move 处理,可以在这里添加 + canvas1.on('mouse:move', (e) => { + if (!e.pointer || typeof background_image === 'undefined' || !background_image) return; + let width = canvas.getWidth() + let height = canvas.getHeight() + $('#posText').text( + 'X: ' + (e.pointer.x - (width / 2 - (width / 2 - background_image.left) * zoom)).toFixed(0) + ' Y: ' + (e.pointer.y - (height / 2 - (height / 2 - background_image.top) * zoom)).toFixed(0) + ) + }) + // 注意:以下事件已在 core.js 的 initCanvasEvents() 中统一处理,移除重复监听以避免冲突: + // - mouse:down (通过 handleObjectSelected 处理) + // - object:scaling, object:rotating, object:rotated + // - text:editing:entered, text:editing:exited + // - object:modified (已在 core.js 中处理) + // - mouse:up (已在 core.js 中处理) + // 如果需要 canvas1 特定的处理逻辑,请修改 core.js 中的相应函数 + + // 保留 canvas1 特定的 after:render 处理 + canvas1.on('after:render', function () { + if (is_bgi_add) { + var image = document.getElementById('source_front') + ctx1.drawImage(image, (canvas.width - $('#source_front').width()) / 2, (canvas.height - $('#source_front').height()) / 2, $('#source_front').width(), $('#source_front').height()) + } + }) + // 保留 canvas1 特定的 text:changed 处理 + canvas1.on('text:changed', (e) => { + $('#text_text').val(e.target.get('text')) + }) + // 保留 canvas2 特定的 after:render 处理 + canvas2.on('after:render', function () { + if (is_bgi_add) { + var image = document.getElementById('source_back') + ctx2.drawImage(image, (canvas.width - $('#source_back').width()) / 2, (canvas1.height - $('#source_back').height()) / 2, $('#source_back').width(), $('#source_back').height()) + } + }) + // 注意:以下事件已在 core.js 的 initCanvasEvents() 中统一处理,移除重复监听以避免冲突: + // - mouse:down (通过 handleObjectSelected 处理) + // - mouse:up (已在 core.js 中处理) + // - object:scaling, object:rotating, object:rotated, object:added + // - text:editing:entered, text:editing:exited + // - object:modified (已在 core.js 中处理) + // 如果需要 canvas2 特定的处理逻辑,请修改 core.js 中的相应函数 +} diff --git a/frontend-web/js/index.js b/frontend-web/js/index.js new file mode 100644 index 0000000..49bc16e --- /dev/null +++ b/frontend-web/js/index.js @@ -0,0 +1,737 @@ +function get_filename(filePath) { + + if (!filePath) return ""; + + return filePath.split(/[/\\]/).pop(); + +} + + + +function escapeAttr(s) { + + if (s == null) return ""; + + return String(s).replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); + +} + + + +var SOON_EMPTY_IMG = '../assets/images/web/empty-state.png'; + + + +function soonEmptyBlock(title, hint, extraHtml) { + + var html = '
    ' + + + '' + + + '

    ' + escapeAttr(title) + '

    '; + + if (hint) { + + html += '

    ' + escapeAttr(hint) + '

    '; + + } + + if (extraHtml) html += extraHtml; + + html += '
    '; + + return html; + +} + + + +let s_lan = "zh"; + + + +function language_str(str) { + + let t = { + + "noFile": { zh: "当前文件不存在!", ozh: "This file does not exist!", en: "File not found!" }, + + "saveTime": { zh: "保存时间:", ozh: "保存時間:", en: "Saved on:" }, + + "selectFile": { zh: "请选择文件", ozh: "請選擇文件", en: "Please select a file" }, + + "comfirm": { zh: "确认", ozh: "確認", en: "Confirm" }, + + "cancel": { zh: "取消", ozh: "取消", en: "Cancel" }, + + "SDFile": { zh: "SoonDesign模板文件", ozh: "SoonDesign模闆文件", en: "SoonDesign Template File" }, + + "lost": { zh: "(文件已丢失)", ozh: "(文件已丟失)", en: "(File Lost)" }, + + "clickToDelete": { zh: "文件已丢失,点击删除此记录", ozh: "文件已丟失,點擊刪除此記錄", en: "File lost, click to remove" }, + + "deleted": { zh: "已删除", ozh: "已刪除", en: "Deleted" }, + + "whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Save current file?" }, + + "delTitle": { zh: "警告", ozh: "警告", en: "Warning" }, + + "delContent": { zh: "文件已丢失,确认是否删除?", ozh: "文件已丟失,確認是否刪除?", en: "File lost, confirm delete?" }, + + "deleteFileConfirm": { zh: "确认删除该文件?删除后无法恢复。", ozh: "確認刪除該文件?刪除後無法恢復。", en: "Delete this file? This cannot be undone." }, + + "deleteBtn": { zh: "删除", ozh: "刪除", en: "Delete" } + + }; + + return t[str][s_lan] || t[str]['zh']; + +} + + + +function openDesign(type, filePath) { + + if (window.platformBridge && window.platformBridge.openDesignPage) { + + window.platformBridge.openDesignPage(filePath || '', type); + + } + +} + + + +layui.use(['layer', 'form', 'jquery'], function () { + + var $ = layui.$; + + var layer = layui.layer; + + layer.config({ skin: 'soon-layer' }); + + + + if (window.sysAPI && window.sysAPI.getAppVersion) { + + window.sysAPI.getAppVersion().then(function (ver) { + + $("#app-version").text("v" + ver); + + }).catch(function () {}); + + } + + + + function langua_ge(lan) { + + lan = lan || 'zh'; + + $("[language='m']").each(function () { + + $(this).html($(this).attr(lan)); + + }); + + $("[language='t']").each(function () { + + $(this).attr("title", $(this).attr(lan)); + + }); + + } + + + + var lang = localStorage.getItem("lang"); + + if (lang) { + + s_lan = lang; + + langua_ge(s_lan); + + $("#language_select").val(s_lan); + + } else if (window.platformBridge && window.platformBridge.getLocale) { + + window.platformBridge.getLocale().then(function (loc) { + + if (loc && loc.indexOf('zh') === 0) { + + s_lan = loc.indexOf('TW') >= 0 ? 'ozh' : 'zh'; + + } else { + + s_lan = 'en'; + + } + + langua_ge(s_lan); + + $("#language_select").val(s_lan); + + localStorage.setItem("lang", s_lan); + + }); + + } + + + + $('#language_select').on('change', function () { + + langua_ge($(this).find('option:selected').val()); + + s_lan = $(this).find('option:selected').val(); + + localStorage.setItem("lang", s_lan); + + loadHistory(); + + }); + + + + function hasCloudAuth() { + if (typeof window.soonGetAccessToken === 'function') return !!window.soonGetAccessToken(); + try { return !!localStorage.getItem('soon_access'); } catch (e) { return false; } + } + + function makeFileKey(id, version) { + return typeof window.soonMakeFileKey === 'function' + ? window.soonMakeFileKey(id, version) + : ('soondesign_file:' + id + ':v' + version); + } + + var fileListState = { page: 1, size: 12, total: 0 }; + var templateListState = { page: 1, size: 8, total: 0, items: [] }; + + function renderFilePager() { + var el = document.getElementById('filePager'); + if (!el) return; + var page = fileListState.page; + var size = fileListState.size; + var total = fileListState.total; + var pages = Math.max(1, Math.ceil(total / size)); + if (total === 0) { + el.innerHTML = ''; + return; + } + el.innerHTML = + '
    ' + + '' + + '第 ' + page + ' / ' + pages + ' 页,共 ' + total + ' 个' + + '
    '; + var prev = document.getElementById('filePrev'); + var next = document.getElementById('fileNext'); + if (prev && !prev.disabled) { + prev.onclick = function () { + fileListState.page = Math.max(1, fileListState.page - 1); + loadHistory(); + }; + } + if (next && !next.disabled) { + next.onclick = function () { + fileListState.page = Math.min(pages, fileListState.page + 1); + loadHistory(); + }; + } + } + + async function loadHistory() { + + try { + + var recentCountEl = document.getElementById('recentCount'); + var filePagerEl = document.getElementById('filePager'); + + if (!hasCloudAuth()) { + + if (recentCountEl) recentCountEl.textContent = '0'; + if (filePagerEl) filePagerEl.innerHTML = ''; + + $(".card-list").html(soonEmptyBlock('登录后查看文件', '请先登录以管理云端设计文件', + + '去登录')); + + return; + + } + + if (!window.platformBridge || !window.platformBridge.listCloudFiles) { + + $(".card-list").html(""); + + return; + + } + + var data = await window.platformBridge.listCloudFiles({ + page: fileListState.page, + size: fileListState.size, + }); + + var items = (data && data.items) ? data.items : []; + fileListState.total = (data && data.total != null) ? data.total : items.length; + if (data && data.page) fileListState.page = data.page; + + if (recentCountEl) recentCountEl.textContent = String(fileListState.total); + + if (!items.length) { + + $(".card-list").html(soonEmptyBlock('暂无文件', '点击「打开文件」导入,或新建模板开始设计')); + renderFilePager(); + return; + + } + + let h = ""; + + for (let item of items) { + + let filePath = makeFileKey(item.id, item.version); + + let src = ""; + + let fileExists = true; + + let imgStyle = ""; + + let realType = 1; + + const soonData = await window.sysAPI.readJsonFile(filePath); + + if (soonData) { + + realType = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1); + + var fallbackThumb = (realType == 2 || realType == "2") ? soonAsset('bg_2.png') : soonAsset('bg_1.png'); + + src = typeof soonSafeImageUrl === 'function' + + ? soonSafeImageUrl(soonData.frontDisplayPic, fallbackThumb) + + : (soonData.frontDisplayPic || fallbackThumb); + + if (!src) src = fallbackThumb; + + } else { + + fileExists = false; + + src = soonAsset('bg_1.png'); + + imgStyle = "opacity: 0.6; filter: grayscale(100%);"; + + } + + let displayName = item.name || ('文件 #' + item.id); + + let cardTitle = displayName; + + var cardClass = 'card'; + + if (!fileExists) { + + cardTitle = language_str("clickToDelete"); + + cardClass += ' soon-card--lost'; + + } + + var lostBadge = !fileExists + + ? ' ' + language_str("lost") + '' + + : ''; + + var dlBtn = ''; + + var delBtn = ''; + + h += `
    + +
    + +
    ${dlBtn}${delBtn}
    + + + +
    ${escapeAttr(displayName)}${lostBadge}
    + +
    + +
    `; + + } + + $(".card-list").html(h); + renderFilePager(); + + } catch (e) { + + console.error("加载文件列表出错", e); + + $(".card-list").html(soonEmptyBlock('加载失败', '请刷新页面重试', + '')); + var retry = document.getElementById('fileListRetry'); + if (retry) retry.onclick = function () { loadHistory(); }; + if (document.getElementById('filePager')) document.getElementById('filePager').innerHTML = ''; + + } + + } + + + + function renderTemplatesError(message) { + + var grid = document.getElementById('templatesGrid'); + + var countEl = document.getElementById('templateCount'); + + if (!grid) return; + + templateListState.items = []; + templateListState.total = 0; + templateListState.page = 1; + + grid.innerHTML = soonEmptyBlock(message, null, + ''); + + if (countEl) countEl.textContent = '!'; + + renderTemplatePager(); + + var btn = document.getElementById('templatesRetry'); + + if (btn) btn.onclick = loadTemplates; + + } + + function templateCardHtml(m, i) { + var defaultThumb = soonAsset('bg_1.png'); + var thumbSrc = typeof soonSafeImageUrl === 'function' + ? soonSafeImageUrl(m.thumbnail_url, defaultThumb) + : (m.thumbnail_url || defaultThumb); + if (!thumbSrc) thumbSrc = defaultThumb; + var esc = typeof soonEscapeHtml === 'function' ? soonEscapeHtml : escapeAttr; + var name = esc(m.name || m.title || '模板'); + return '
    ' + + '' + + '' + name + '
    '; + } + + function bindTemplateCards(grid) { + if (!grid) return; + grid.querySelectorAll('.soon-template-card').forEach(function (card) { + card.onclick = function () { + openDesign(Number(card.dataset.type) || 1, ''); + }; + }); + } + + function renderTemplatePager() { + var prevNav = document.getElementById('templatePrev'); + var nextNav = document.getElementById('templateNext'); + var page = templateListState.page; + var size = templateListState.size; + var total = templateListState.total; + var pages = Math.max(1, Math.ceil(total / size)); + + if (prevNav) prevNav.disabled = total === 0 || page <= 1; + if (nextNav) nextNav.disabled = total === 0 || page >= pages; + } + + function goTemplatePage(page) { + var pages = Math.max(1, Math.ceil(templateListState.total / templateListState.size)); + templateListState.page = Math.max(1, Math.min(pages, page)); + renderTemplatesPage(); + } + + function renderTemplatesPage() { + var grid = document.getElementById('templatesGrid'); + var countEl = document.getElementById('templateCount'); + if (!grid) return; + + var items = templateListState.items || []; + templateListState.total = items.length; + if (countEl) countEl.textContent = String(items.length); + + if (!items.length) { + grid.innerHTML = soonEmptyBlock('暂无模板'); + renderTemplatePager(); + return; + } + + var start = (templateListState.page - 1) * templateListState.size; + var slice = items.slice(start, start + templateListState.size); + grid.innerHTML = slice.map(function (m, i) { return templateCardHtml(m, i); }).join(''); + bindTemplateCards(grid); + renderTemplatePager(); + } + + async function loadTemplates() { + + var grid = document.getElementById('templatesGrid'); + + var countEl = document.getElementById('templateCount'); + + if (!grid) return; + + var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || ''; + + if (!base) { + + templateListState.items = []; + templateListState.total = 0; + templateListState.page = 1; + grid.innerHTML = soonEmptyBlock('暂不可用', '请稍后重试'); + if (countEl) countEl.textContent = '0'; + renderTemplatePager(); + return; + + } + + try { + + var r = await fetch(base + '/soon-models'); + + var j = await r.json(); + + if (!j.ok) { + + renderTemplatesError(j.message || '模板加载失败'); + + return; + + } + + var items = (j.data && j.data.items) ? j.data.items : []; + templateListState.items = items; + templateListState.total = items.length; + templateListState.page = 1; + renderTemplatesPage(); + + } catch (e) { + + renderTemplatesError('网络错误,请稍后重试'); + + } + + } + + + + loadHistory(); + + loadTemplates(); + + var templatePrevNav = document.getElementById('templatePrev'); + var templateNextNav = document.getElementById('templateNext'); + if (templatePrevNav) { + templatePrevNav.onclick = function () { + if (!templatePrevNav.disabled) goTemplatePage(templateListState.page - 1); + }; + } + if (templateNextNav) { + templateNextNav.onclick = function () { + if (!templateNextNav.disabled) goTemplatePage(templateListState.page + 1); + }; + } + + $("#openfile").click(function () { OpenDialog(); }); + + $("#new1").click(function () { + + if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('新建')) return; + + openDesign(1, ''); + + }); + + $("#new2").click(function () { + + if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('新建')) return; + + openDesign(2, ''); + + }); + + + + function confirmDeleteCloudFile(fileId, message, onDone) { + if (!fileId) return; + layer.open({ + type: 1, + skin: 'soon-layer', + title: language_str("delTitle"), + content: '
    ' + message + '
    ', + btn: [language_str("comfirm"), language_str("cancel")], + btnAlign: 'r', + area: ['320px', 'auto'], + resize: false, + shadeClose: true, + yes: async function (index) { + if (window.platformBridge && window.platformBridge.deleteCloudFile) { + try { + await window.platformBridge.deleteCloudFile(fileId); + } catch (e) { /* ignore */ } + } + layer.msg(language_str("deleted"), { icon: 1, time: 1000 }); + if (typeof onDone === 'function') onDone(); + layer.close(index); + } + }); + } + + $(".card-list").on("click", '.soon-file-card__download', function (e) { + + e.stopPropagation(); + + var card = $(this).closest('.card'); + + var id = parseInt(card.attr('data-id'), 10); + + var name = card.attr('data-name') || 'design.soon'; + + if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('下载')) return; + + if (id && window.platformBridge && window.platformBridge.downloadCloudFile) { + + window.platformBridge.downloadCloudFile(id, name).catch(function () {}); + + } + + }); + + $(".card-list").on("click", '.soon-file-card__delete', function (e) { + + e.stopPropagation(); + + var card = $(this).closest('.card'); + + var fileId = parseInt(card.attr('data-id'), 10); + + if (!fileId) return; + + confirmDeleteCloudFile(fileId, language_str('deleteFileConfirm'), loadHistory); + + }); + + $(".card-list").on("click", '.card', async function () { + + let filePath = $(this).attr("data-file") || $(this).attr("data"); + + const soonData = await window.sysAPI.readJsonFile(filePath); + + if (soonData) { + + let type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1); + + if (typeof window.soonBindFileMeta === 'function') { + window.soonBindFileMeta(filePath, { name: $(this).attr('data-name') || '' }); + } + + openDesign(type, filePath); + + } else { + + var fileId = parseInt($(this).attr('data-id'), 10); + + confirmDeleteCloudFile(fileId, language_str("delContent"), loadHistory); + + } + + }); + + + + function OpenDialog() { + + if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return; + + var dialogApi = window.platformBridge && window.platformBridge.showOpenDialog + + ? { showOpenDialog: function (opts) { return window.platformBridge.showOpenDialog(opts); } } + + : null; + + if (!dialogApi) return; + + dialogApi.showOpenDialog({ + + title: language_str("selectFile"), + + buttonLabel: language_str("comfirm"), + + filters: [{ name: language_str("SDFile"), extensions: ['soon'] }] + + }).then(async function (result) { + + if (result.canceled) return; + + var file = (result.files && result.files[0]) || result.file; + + if (!file || !file.text) { + + layer.msg(language_str("noFile")); + + return; + + } + + var fileName = file.name || 'design.soon'; + + var soonData; + + try { + + soonData = JSON.parse(await file.text()); + + } catch (e) { + + layer.msg(language_str("noFile")); + + return; + + } + + if (!window.platformBridge || !window.platformBridge.importSoonFile) return; + + try { + + var res = await window.platformBridge.importSoonFile(fileName, soonData); + + if (!res || !res.fileKey) return; + + var type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1); + + openDesign(type, res.fileKey); + + loadHistory(); + + } catch (err) { /* errors shown by bridge */ } + + }).catch(function (err) { console.log(err); }); + + } + +}); + diff --git a/frontend-web/js/member.js b/frontend-web/js/member.js new file mode 100644 index 0000000..75c867c --- /dev/null +++ b/frontend-web/js/member.js @@ -0,0 +1,471 @@ +(function () { + 'use strict'; + + var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '/api/v1'; + var currentMembership = null; + var orderState = { page: 1, size: 8, total: 0 }; + + var esc = typeof soonEscapeHtml === 'function' ? soonEscapeHtml : function (s) { + if (s == null) return ''; + return String(s); + }; + + var STATUS_LABEL = { + pending: '待支付', + paid: '已支付', + cancelled: '已取消', + refunded: '已退款', + }; + + var CHANNEL_LABEL = { alipay: '支付宝', wechat: '微信' }; + + function apiFetch(url, opts) { + if (typeof soonAuthedFetch === 'function') return soonAuthedFetch(url, opts || {}); + opts = opts || {}; + var token = localStorage.getItem('soon_access') || ''; + opts.headers = Object.assign({}, opts.headers || {}); + if (token) opts.headers.Authorization = 'Bearer ' + token; + return fetch(url, opts); + } + + function parseApiJson(r) { + return r.text().then(function (text) { + try { + return JSON.parse(text); + } catch (e) { + return { ok: false, message: '服务暂时不可用,请稍后重试' }; + } + }); + } + + function apiGet(p) { + return apiFetch(base + p).then(function (r) { + if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' }; + return parseApiJson(r); + }); + } + + function apiPost(p, body) { + return apiFetch(base + p, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}), + }).then(function (r) { + if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' }; + return parseApiJson(r); + }); + } + + function usageMetrics(usage, quotaMb) { + var bytes = (usage && usage.storage_bytes) || 0; + var totalBytes = (quotaMb || 0) * 1024 * 1024; + var pctNum = totalBytes > 0 ? (bytes / totalBytes) * 100 : 0; + var pctLabel = pctNum > 0 && pctNum < 1 ? '<1' : String(Math.min(100, Math.round(pctNum))); + var barWidth = bytes > 0 ? Math.max(pctNum < 1 ? 0.8 : pctNum, 0.8) : 0; + return { + pctNum: pctNum, + pctLabel: pctLabel, + barWidth: Math.min(100, barWidth), + usedDisplay: (usage && usage.used_display) || '0 MB', + }; + } + + function showPayBanner() { + var b = document.getElementById('payBanner'); + if (!b) return; + b.classList.add('is-visible'); + setTimeout(function () { b.classList.remove('is-visible'); }, 5000); + } + + function initLanguageSelect() { + var sel = document.getElementById('language_select'); + if (!sel) return; + sel.value = localStorage.getItem('lang') || 'zh'; + sel.onchange = function () { localStorage.setItem('lang', sel.value); }; + } + + function isPaidMember(m) { + if (!m) return false; + if (m.is_member === true) return true; + return m.tier === 'member' || m.tier === 'pro'; + } + + function statusPill(sub, isMember) { + if (!isMember) { + return '免费版'; + } + var st = (sub && sub.status) || 'active'; + var cls = 'soon-member-status-pill--active'; + var text = '生效中'; + if (st === 'expiring') { cls = 'soon-member-status-pill--expiring'; text = '即将到期'; } + return '' + text + ''; + } + + function storageRing(metrics, warn) { + var r = 52; + var c = 2 * Math.PI * r; + var offset = c - (c * Math.min(metrics.pctNum, 100) / 100); + var fillCls = warn ? ' soon-member-storage-ring__fill--warn' : ''; + return '
    ' + + '' + + '
    ' + + '' + metrics.pctLabel + '%' + + '已使用
    '; + } + + function renderMyPlan(m) { + var el = document.getElementById('myPlan'); + if (!el) return; + if (!m) { + el.innerHTML = '
    ' + + '

    加载失败,请重试

    ' + + '
    '; + var retry = document.getElementById('myPlanRetry'); + if (retry) retry.onclick = loadMembership; + return; + } + currentMembership = m; + var isMember = isPaidMember(m); + if (typeof window.soonApplyMembership === 'function') { + window.soonApplyMembership(m); + } else if (typeof window.soonLoadMembership === 'function') { + window.soonLoadMembership(true); + } + var usage = m.usage || {}; + var sub = m.subscription || {}; + + if (!isMember) { + el.innerHTML = + '
    ' + + '
    ' + + '
    ' + + '免费版' + statusPill(sub, false) + '
    ' + + '

    ' + esc(m.name || '免费版') + '

    ' + + '

    ' + esc(m.description || '免费体验设计与编辑') + '

    ' + + '

    订阅后可预览、打印、保存并导出作品

    ' + + '
    ' + + '' + + '
    '; + var goPlans = document.getElementById('myPlanGoPlans'); + if (goPlans) { + goPlans.onclick = function () { + var sec = document.getElementById('plans'); + if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }; + } + return; + } + + var quota = m.quota_mb || 2048; + var metrics = usageMetrics(usage, quota); + var warn = metrics.pctNum >= 90; + var barCls = warn ? 'soon-member-progress__bar soon-member-progress__bar--warn' : 'soon-member-progress__bar'; + var expires = sub.expires_at + ? esc(String(sub.expires_at).slice(0, 16).replace('T', ' ')) + : '—'; + var days = sub.days_remaining != null ? sub.days_remaining + ' 天' : '—'; + + el.innerHTML = + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '订阅版' + statusPill(sub, true) + '
    ' + + '

    ' + esc(m.name) + '

    ' + + '

    ' + esc(m.description || '已解锁完整交付能力') + '

    ' + + '

    有效期至 ' + expires + ' · 剩余 ' + esc(days) + '

    ' + + storageRing(metrics, warn) + '
    ' + + '
    ' + + '
    云端存储占用' + + esc(metrics.usedDisplay) + ' / ' + esc(m.quota_display || quota + ' MB') + + '(' + metrics.pctLabel + '%)
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + (usage.files_count || 0) + + '
    云端文件
    ' + + '
    ' + esc(days) + + '
    剩余天数
    ' + + '
    '; + } + + function orderBadge(status, refundStatus) { + if (refundStatus === 'pending') { + return '退款审核'; + } + var cls = 'soon-member-order-badge--' + (status || 'pending'); + return '' + esc(STATUS_LABEL[status] || status) + ''; + } + + function orderPagerHtml() { + var page = orderState.page; + var size = orderState.size; + var total = orderState.total; + var pages = Math.max(1, Math.ceil(total / size)); + if (total === 0) return ''; + return '
    ' + + '' + + '第 ' + page + ' / ' + pages + ' 页(共 ' + total + ' 条)' + + '
    '; + } + + function bindOrderPager() { + var prev = document.getElementById('orderPrev'); + var next = document.getElementById('orderNext'); + if (prev && !prev.disabled) { + prev.onclick = function () { + orderState.page = Math.max(1, orderState.page - 1); + loadOrders(); + }; + } + if (next && !next.disabled) { + next.onclick = function () { + var pages = Math.max(1, Math.ceil(orderState.total / orderState.size)); + orderState.page = Math.min(pages, orderState.page + 1); + loadOrders(); + }; + } + } + + function orderActionCell(o) { + var actions = []; + if (o.status === 'pending') { + actions.push(''); + actions.push(''); + } else if (o.status === 'paid' && (!o.refund_status || o.refund_status === 'none')) { + actions.push(''); + } + if (!actions.length) return ''; + return '
    ' + actions.join('') + '
    '; + } + + function bindOrderActions() { + document.querySelectorAll('.soon-member-order-act[data-act]').forEach(function (btn) { + btn.onclick = function () { + var act = btn.dataset.act; + var no = btn.dataset.no; + if (act === 'pay') { + openPayModal(null, btn.dataset.plan, btn.dataset.price, { + orderNo: no, + channel: btn.dataset.channel || 'wechat', + }); + return; + } + if (act === 'cancel') { + layer.confirm('确定取消该待支付订单?', { skin: 'soon-layer', title: '取消订单' }, function (idx) { + apiPost('/pay/orders/' + encodeURIComponent(no) + '/cancel', {}).then(function (r) { + layer.close(idx); + if (r.ok) { + soonToast('订单已取消', 'success'); + loadOrders(); + } else { + soonToast(r.message || '取消失败', 'warn'); + } + }); + }); + return; + } + if (act === 'refund') { + layer.prompt({ + skin: 'soon-layer', + title: '申请退款', + formType: 2, + value: '', + maxlength: 200, + }, function (reason, idx) { + reason = (reason || '').trim(); + if (!reason) { + soonToast('请填写退款原因', 'warn'); + return; + } + apiPost('/pay/orders/' + encodeURIComponent(no) + '/refund-request', { reason: reason }).then(function (r) { + layer.close(idx); + if (r.ok) { + soonToast('退款申请已提交', 'success'); + loadOrders(); + } else { + soonToast(r.message || '提交失败', 'warn'); + } + }); + }); + } + }; + }); + } + + function renderOrders(orders) { + var el = document.getElementById('orderHistory'); + if (!el) return; + if (!orders || !orders.length) { + el.innerHTML = '
    暂无订单,订阅后将显示在这里
    ' + + orderPagerHtml(); + bindOrderPager(); + return; + } + var rows = orders.map(function (o) { + return '' + esc(o.order_no) + '' + esc(o.plan_name) + '¥' + + (o.amount_cents / 100).toFixed(2) + '' + esc(CHANNEL_LABEL[o.channel] || o.channel) + + '' + orderBadge(o.status, o.refund_status) + '' + + esc(String(o.paid_at || o.created_at || '').slice(0, 16).replace('T', ' ')) + '' + + orderActionCell(o) + ''; + }).join(''); + el.innerHTML = '' + + rows + '
    订单号方案金额渠道状态时间操作
    ' + orderPagerHtml(); + bindOrderPager(); + bindOrderActions(); + } + + function loadOrders() { + apiGet('/pay/orders?page=' + orderState.page + '&size=' + orderState.size).then(function (res) { + if (!res.ok) { + renderOrders([]); + return; + } + var data = res.data || {}; + orderState.total = data.total || 0; + orderState.page = data.page || orderState.page; + orderState.size = data.size || orderState.size; + renderOrders(data.items || []); + }).catch(function () { + renderOrders([]); + }); + } + + function renderPlans(items) { + var grid = document.getElementById('plans'); + if (!grid || !window.SoonMemberPlan) return; + var paid = items.filter(function (p) { return p.code !== 'free' && p.price_cents > 0; }); + var currentCode = currentMembership && isPaidMember(currentMembership) ? currentMembership.code : ''; + if (!paid.length) { + grid.innerHTML = '
    暂无可用订阅方案
    '; + return; + } + grid.innerHTML = window.SoonMemberPlan.plansGridHtml(paid, { + allPlans: paid, + currentCode: currentCode, + isMember: isPaidMember(currentMembership), + mode: 'page', + }); + grid.querySelectorAll('.soon-plan-card__cta[data-id]').forEach(function (btn) { + btn.onclick = function () { + openPayModal(Number(btn.dataset.id), btn.dataset.name, btn.dataset.price); + }; + }); + } + + function renderPlansError(message, onRetry) { + var grid = document.getElementById('plans'); + if (!grid) return; + grid.innerHTML = '
    ' + + esc(message || '方案加载失败') + + '
    '; + var btn = document.getElementById('plansRetry'); + if (btn && onRetry) btn.onclick = onRetry; + } + + function loadPlans() { + apiGet('/plans').then(function (ps) { + if (!ps.ok) { + renderPlansError(ps.message || '方案加载失败', loadPlans); + return; + } + renderPlans((ps.data && ps.data.items) || []); + }).catch(function () { + renderPlansError('网络错误,请稍后重试', loadPlans); + }); + } + + function openPayModal(planId, planName, priceDisplay, opts) { + opts = opts || {}; + var payCore = window.SoonMemberPay; + if (!payCore || !payCore.openPayModal) { + soonToast('支付模块未加载,请刷新页面', 'warn'); + return; + } + payCore.openPayModal({ + planId: planId, + planName: planName, + priceDisplay: priceDisplay, + orderNo: opts.orderNo, + channel: opts.channel || 'wechat', + onPaid: function () { + showPayBanner(); + loadMembership(); + }, + }); + } + + function loadMembership() { + apiGet('/plans/me').then(function (my) { + if (!my.ok || !my.data) { + renderMyPlan(null); + return; + } + renderMyPlan(my.data.membership || my.data); + loadOrders(); + loadPlans(); + }).catch(function () { + renderMyPlan(null); + }); + } + + function init() { + if (!localStorage.getItem('soon_access')) { + location.href = 'login.web.html?redirect=' + encodeURIComponent('member.web.html'); + return; + } + if (window.SoonPortalTopbar) { + SoonPortalTopbar.mount('#portal-topbar', { active: 'member' }); + } else { + initLanguageSelect(); + } + soonPortalAuth.init({ logoutReload: false }); + apiGet('/auth/me').then(function (me) { + if (!me.ok) { + location.href = 'login.web.html?redirect=' + encodeURIComponent('member.web.html'); + return; + } + if (me.data && me.data.role === 'admin') { + var adminEl = document.getElementById('auth_admin'); + if (adminEl) adminEl.style.display = 'inline-flex'; + } + loadMembership(); + if (new URLSearchParams(location.search).get('paid') || sessionStorage.getItem('soon_pay_return')) { + sessionStorage.removeItem('soon_pay_return'); + showPayBanner(); + loadMembership(); + } + }).catch(function () { + renderMyPlan(null); + renderPlansError('网络错误,请稍后重试', function () { location.reload(); }); + var ordersEl = document.getElementById('orderHistory'); + if (ordersEl) { + ordersEl.innerHTML = '
    加载失败,
    '; + var ordersRetry = document.getElementById('ordersRetry'); + if (ordersRetry) ordersRetry.onclick = function () { location.reload(); }; + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/lib/platform/bridge.js b/frontend-web/js/platform/bridge.js similarity index 100% rename from lib/platform/bridge.js rename to frontend-web/js/platform/bridge.js diff --git a/frontend-web/js/platform/web.js b/frontend-web/js/platform/web.js new file mode 100644 index 0000000..ef60f44 --- /dev/null +++ b/frontend-web/js/platform/web.js @@ -0,0 +1,461 @@ +(function () { + 'use strict'; + + var HISTORY_KEY = 'soondesign_history'; + var VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '3.2.101'; + + function getAppBasePathname() { + var p = (typeof window !== 'undefined' && window.location && window.location.pathname) ? window.location.pathname : '/'; + while (p.length > 1 && p.endsWith('/')) { + p = p.slice(0, -1); + } + if (/\.web\.html$/i.test(p) || /\.html$/i.test(p)) { + p = p.replace(/[^/]+$/, ''); + } + if (!p.endsWith('/')) p += '/'; + return p; + } + + var SERVER_API_BASE = (function () { + var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base); + if (cfg) return String(cfg).replace(/\/+$/, '') + '/'; + if (typeof window === 'undefined' || !window.location) return '/api/v1/'; + return window.location.origin + getAppBasePathname() + 'api/v1/'; + })(); + + function getAccessToken() { + if (typeof window.soonGetAccessToken === 'function') return window.soonGetAccessToken(); + try { return localStorage.getItem('soon_access') || ''; } catch (e) { return ''; } + } + + function authedFetch(path, opts) { + var url = SERVER_API_BASE + String(path).replace(/^\/+/, ''); + if (typeof window.soonAuthedFetch === 'function') { + return window.soonAuthedFetch(url, opts); + } + opts = opts || {}; + opts.headers = Object.assign({}, opts.headers || {}); + if (!opts.headers['Authorization'] && !opts.headers['authorization']) { + var t = getAccessToken(); + if (t) opts.headers['Authorization'] = 'Bearer ' + t; + } + return fetch(url, opts); + } + + var JSON_HEADERS = { + 'Content-Type': 'application/json; charset=utf-8', + 'Accept': 'application/json' + }; + + function parseCloudRef(p) { + return typeof window.soonParseFileKey === 'function' ? window.soonParseFileKey(p) : null; + } + + function normalizeSoonName(p) { + return typeof window.soonNormalizeSoonName === 'function' + ? window.soonNormalizeSoonName(p) + : String(p || 'design.soon'); + } + + function packCloudResult(j) { + if (!j || !j.ok || !j.data) { + return Promise.reject(new Error('cloud_empty_response')); + } + var fileKey = typeof window.soonApplyCloudMeta === 'function' + ? window.soonApplyCloudMeta(j.data) + : null; + if (!fileKey) { + var d = j.data; + window._soonFileMeta = { id: d.id, version: d.version, name: d.name }; + fileKey = 'soondesign_file:' + d.id + ':v' + d.version; + } + return { fileId: j.data.id, version: j.data.version, fileKey: fileKey, name: j.data.name }; + } + + function handleCloudResponse(response, bodyText) { + if (response.ok) { + try { return Promise.resolve(JSON.parse(bodyText)); } catch (e) { return Promise.resolve(null); } + } + var info = typeof window.soonParseApiError === 'function' + ? window.soonParseApiError(response, bodyText) + : { status: response.status, message: '操作失败' }; + if (typeof window.soonShowApiError === 'function') window.soonShowApiError(info); + var err = new Error(info.message || 'cloud_error'); + err.status = info.status; + err.code = info.code; + return Promise.reject(err); + } + + function requireCloudAuth(actionLabel) { + if (getAccessToken()) return true; + if (typeof window.soonRequireLogin === 'function') return window.soonRequireLogin(actionLabel); + return false; + } + + function navigateToPage(pathWithQuery) { + try { + location.href = new URL(pathWithQuery, window.location.href).href; + } catch (e) { + location.href = pathWithQuery; + } + } + + function getLocale() { + var lang = typeof navigator !== 'undefined' ? (navigator.language || navigator.browserLanguage || '') : ''; + if (lang.indexOf('zh') === 0) return lang.indexOf('TW') >= 0 ? 'ozh' : 'zh'; + return 'en'; + } + + function getSystemFonts() { + var list = [ + 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia', + 'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong', + 'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB' + ]; + if (typeof document !== 'undefined' && document.fonts && document.fonts.forEach) { + var set = {}; + document.fonts.forEach(function (f) { + var name = (f.family || '').replace(/^["']|["']$/g, ''); + if (name) set[name] = 1; + }); + list = Object.keys(set).length ? Object.keys(set).sort() : list; + } + return Promise.resolve(list); + } + + function listCloudFiles(pageOrLimit, sizeOrOffset) { + if (!requireCloudAuth('查看文件')) return Promise.reject(new Error('unauthorized')); + var url; + var fallback = { items: [], total: 0 }; + if (typeof pageOrLimit === 'object' && pageOrLimit) { + var page = pageOrLimit.page || 1; + var size = pageOrLimit.size || 12; + url = 'files?page=' + page + '&size=' + size; + fallback = { items: [], total: 0, page: page, size: size }; + } else { + var lim = pageOrLimit || 50; + var off = sizeOrOffset || 0; + url = 'files?limit=' + lim + '&offset=' + off; + fallback = { items: [], limit: lim, offset: off, total: 0 }; + } + return authedFetch(url, { headers: { Accept: 'application/json' } }) + .then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); }) + .then(function (j) { return (j && j.ok && j.data) ? j.data : fallback; }); + } + + function createCloudFile(name, jsonStr) { + if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized')); + if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('保存')) { + return Promise.reject(new Error('membership_required')); + } + return authedFetch('files', { + method: 'POST', + headers: JSON_HEADERS, + body: JSON.stringify({ name: normalizeSoonName(name), json: jsonStr }) + }) + .then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); }) + .then(packCloudResult); + } + + function updateCloudFile(id, name, jsonStr, version) { + if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized')); + if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('保存')) { + return Promise.reject(new Error('membership_required')); + } + var body = { name: normalizeSoonName(name), json: jsonStr }; + if (version != null) body.version = version; + return authedFetch('files/' + id, { + method: 'PUT', + headers: JSON_HEADERS, + body: JSON.stringify(body) + }) + .then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); }) + .then(packCloudResult); + } + + function deleteCloudFile(id) { + if (!requireCloudAuth('删除')) return Promise.reject(new Error('unauthorized')); + return authedFetch('files/' + id, { method: 'DELETE', headers: { Accept: 'application/json' } }) + .then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); }); + } + + function importSoonFile(name, jsonObj) { + var jsonStr = typeof jsonObj === 'string' ? jsonObj : JSON.stringify(jsonObj); + return createCloudFile(name, jsonStr); + } + + function downloadCloudFile(id, fileName) { + if (!requireCloudAuth('下载')) return Promise.reject(new Error('unauthorized')); + if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('下载')) { + return Promise.reject(new Error('membership_required')); + } + return authedFetch('files/' + id + '/download', { headers: { Accept: 'application/octet-stream' } }) + .then(function (response) { + if (!response.ok) { + return response.text().then(function (t) { return handleCloudResponse(response, t); }); + } + return response.blob().then(function (blob) { + var a = document.createElement('a'); + a.download = normalizeSoonName(fileName || 'design.soon'); + a.href = URL.createObjectURL(blob); + a.click(); + URL.revokeObjectURL(a.href); + }); + }); + } + + var bridge = { + readHistory: function () { + try { + var raw = localStorage.getItem(HISTORY_KEY); + return Promise.resolve(raw ? JSON.parse(raw) : { history: [] }); + } catch (e) { + return Promise.resolve({ history: [] }); + } + }, + writeHistory: function (data) { + try { + localStorage.setItem(HISTORY_KEY, JSON.stringify(data)); + return Promise.resolve({ success: true }); + } catch (e) { + return Promise.resolve({ success: false, error: e.message }); + } + }, + listCloudFiles: listCloudFiles, + createCloudFile: createCloudFile, + updateCloudFile: updateCloudFile, + deleteCloudFile: deleteCloudFile, + importSoonFile: importSoonFile, + downloadCloudFile: downloadCloudFile, + requireCloudAuth: requireCloudAuth, + readJsonFile: function (pathOrHandle) { + if (!pathOrHandle) return Promise.resolve(null); + if (typeof pathOrHandle === 'object' && pathOrHandle.text) { + return pathOrHandle.text().then(function (t) { + try { return JSON.parse(t); } catch (e) { return null; } + }); + } + var key = typeof pathOrHandle === 'string' ? pathOrHandle : ''; + if (key.indexOf('soondesign_session:') === 0) { + try { + var j = sessionStorage.getItem(key); + if (!j && typeof localStorage !== 'undefined') j = localStorage.getItem(key); + return Promise.resolve(j ? JSON.parse(j) : null); + } catch (e) { return Promise.resolve(null); } + } + if (key && key.indexOf('soondesign_file:') === 0 && typeof fetch !== 'undefined') { + var fileMatch = key.match(/^soondesign_file:(\d+)/); + if (fileMatch && getAccessToken()) { + return authedFetch('files/' + fileMatch[1] + '/download', { headers: { Accept: 'application/json' } }) + .then(function (response) { + if (!response.ok) return null; + return response.text().then(function (text) { + try { return JSON.parse(text); } catch (e) { return null; } + }); + }) + .catch(function () { return null; }); + } + } + return Promise.resolve(null); + }, + showOpenDialog: function (options) { + return new Promise(function (resolve) { + var input = document.createElement('input'); + input.type = 'file'; + if (options && options.filters && Array.isArray(options.filters) && options.filters.length > 0) { + var acceptList = []; + options.filters.forEach(function (filter) { + if (filter.extensions && Array.isArray(filter.extensions)) { + filter.extensions.forEach(function (ext) { + var cleanExt = ext.replace(/^\./, ''); + if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].indexOf(cleanExt.toLowerCase()) >= 0) { + var mimeType = 'image/' + (cleanExt.toLowerCase() === 'jpg' ? 'jpeg' : cleanExt.toLowerCase()); + if (acceptList.indexOf(mimeType) < 0) acceptList.push(mimeType); + } else { + var extWithDot = '.' + cleanExt; + if (acceptList.indexOf(extWithDot) < 0) acceptList.push(extWithDot); + } + }); + } + }); + input.accept = acceptList.length > 0 ? acceptList.join(',') : ''; + } else { + input.accept = '.soon,application/json'; + } + input.style.display = 'none'; + input.onchange = function () { + var f = input.files && input.files[0]; + document.body.removeChild(input); + if (!f) resolve({ canceled: true }); + else resolve({ canceled: false, filePaths: [], files: [f], file: f }); + }; + document.body.appendChild(input); + input.click(); + }); + }, + showSaveDialog: function (options) { + var raw = (options && options.defaultPath) ? options.defaultPath : ''; + var defaultName = normalizeSoonName(raw || 'design.soon'); + if (typeof window.soonDisplayFileName === 'function' && raw.indexOf('soondesign_file:') === 0) { + defaultName = window.soonDisplayFileName(raw); + } + return new Promise(function (resolve) { + var name = typeof prompt === 'function' ? prompt('保存为文件名(如 xxx.soon)', defaultName) : defaultName; + if (name === null) { + resolve({ canceled: true }); + return; + } + var fp = (name && String(name).trim()) ? String(name).trim() : defaultName; + fp = normalizeSoonName(fp); + resolve({ canceled: false, filePath: fp, useCloud: true }); + }); + }, + writeFile: function (pathOrHandle, content) { + var isBlob = content instanceof Blob; + var str = typeof content === 'string' ? content : (isBlob ? null : (content && content.toString ? content.toString() : '')); + + if (isBlob) { + var blobName = typeof pathOrHandle === 'string' ? pathOrHandle : 'output.bin'; + var a = document.createElement('a'); + a.download = blobName; + a.href = URL.createObjectURL(content); + a.click(); + URL.revokeObjectURL(a.href); + return Promise.resolve(); + } + + if (pathOrHandle && pathOrHandle.createWritable) { + pathOrHandle = typeof pathOrHandle.name === 'string' ? pathOrHandle.name : 'design.soon'; + } + + if (!requireCloudAuth('保存')) { + return Promise.reject(new Error('unauthorized')); + } + + var name = typeof pathOrHandle === 'string' ? pathOrHandle : 'design.soon'; + var cloudRef = parseCloudRef(name); + var fileName = normalizeSoonName(name); + + if (cloudRef && cloudRef.id) { + var ver = cloudRef.version; + if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) { + ver = window._soonFileMeta.version; + } + return updateCloudFile(cloudRef.id, fileName, str, ver); + } + + return createCloudFile(fileName, str); + }, + readFile: function (pathOrHandle) { + if (pathOrHandle && pathOrHandle.getFile) { + return pathOrHandle.getFile().then(function (f) { + return new Promise(function (res, rej) { + var r = new FileReader(); + r.onload = function () { res(r.result); }; + r.onerror = rej; + r.readAsArrayBuffer(f); + }); + }); + } + if (pathOrHandle && pathOrHandle.arrayBuffer) { + return pathOrHandle.arrayBuffer(); + } + return Promise.reject(new Error('No file')); + }, + getAppVersion: function () { return Promise.resolve(VERSION); }, + getLocale: getLocale, + getUserDataPath: function () { return ''; }, + getSystemFonts: getSystemFonts, + openDesignPage: function (file, type) { + var t = type || 1; + if (file && typeof sessionStorage !== 'undefined') { + try { + sessionStorage.setItem('soondesign_open_file', file); + sessionStorage.setItem('soondesign_open_type', String(t)); + if (file.indexOf('soondesign_file:') === 0 && window._soonFileMeta) { + sessionStorage.setItem('soondesign_open_meta', JSON.stringify(window._soonFileMeta)); + } + } catch (e) {} + } + var fileParam = file ? encodeURIComponent(file) : ''; + navigateToPage('design' + t + '.web.html?file=' + fileParam + '&type=' + t); + }, + openFirstPage: function () { + navigateToPage('index.web.html'); + }, + onClose: function (callback) { + if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', callback); + } + }, + runClose: function () { + if (typeof window !== 'undefined' && window.close) window.close(); + }, + openHelp: function () { + try { + window.open(new URL('../assets/help/User Manual.pdf', window.location.href).href, '_blank'); + } catch (e) { + window.open('../assets/help/User Manual.pdf', '_blank'); + } + }, + printPdf: function (urlOrBlob) { + var url = urlOrBlob; + if (urlOrBlob && typeof urlOrBlob === 'object' && !(urlOrBlob instanceof String)) { + url = URL.createObjectURL(urlOrBlob); + } + var w = window.open(url, '_blank'); + if (w) w.onload = function () { w.print(); }; + }, + getScaleRate: function () { + return typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1; + }, + clipboard: { + readText: function () { + return navigator.clipboard && navigator.clipboard.readText ? navigator.clipboard.readText() : Promise.resolve(''); + }, + writeText: function (text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + return Promise.resolve(); + } + } + }; + + window.platformBridge = bridge; + + var pathStub = { + join: function () { return [].slice.call(arguments).join('/').replace(/\/+/g, '/'); } + }; + + window.sysAPI = { + readHistory: bridge.readHistory, + writeHistory: bridge.writeHistory, + readJsonFile: bridge.readJsonFile, + getAppVersion: bridge.getAppVersion + }; + window.dialog = { + showOpenDialog: bridge.showOpenDialog, + showSaveDialog: bridge.showSaveDialog + }; + window.path = pathStub; + window.fs = null; + window.remote = null; + var loc = getLocale(); + window.ipcRenderer = { + send: function (ch, a1, a2) { + if (ch === 'get-sys-language') { window._sysLanPending = true; setTimeout(function () { if (window._sysLanCb && window._sysLanPending) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); } + if (ch === 'open-first-page' && bridge.openFirstPage) { bridge.openFirstPage(); } + if (ch === 'open-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); } + if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); } + if (ch === 'run-close' && bridge.runClose) { bridge.runClose(); } + }, + on: function (ch, cb) { + if (ch === 'sys-lan') { window._sysLanCb = cb; if (window._sysLanPending) setTimeout(function () { if (window._sysLanCb) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); } + if (ch === 'close') window._closeCb = cb; + } + }; + window.clipboard = bridge.clipboard; + window.exePath = ''; + window.fullPath = ''; +})(); diff --git a/frontend-web/pages/admin/assets/admin-shell.css b/frontend-web/pages/admin/assets/admin-shell.css new file mode 100644 index 0000000..3a88818 --- /dev/null +++ b/frontend-web/pages/admin/assets/admin-shell.css @@ -0,0 +1,899 @@ +@import url('../../../assets/css/tokens.css'); +@import url('../../../assets/css/components.css'); + +body { + margin: 0; + height: 100vh; + overflow: hidden; + background: var(--soon-bg-deep); + font-family: var(--soon-font); + color: var(--soon-text); +} + +.soon-admin-layout { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.soon-admin-header { + height: 56px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--soon-space-lg); + background: var(--soon-bg-panel); + border-bottom: 1px solid var(--soon-border); + z-index: 100; +} +.soon-admin-header__brand { + display: flex; + align-items: center; + gap: var(--soon-space-sm); + color: var(--soon-text-strong); + font-weight: 600; +} +.soon-admin-header__brand img { width: 28px; height: 28px; } +.soon-admin-header__user { + display: flex; + align-items: center; + gap: var(--soon-space-md); + font-size: 13px; + color: var(--soon-text-muted); +} + +.soon-admin-body { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.soon-admin-sidebar { + width: 220px; + flex-shrink: 0; + background: var(--soon-bg-elevated); + border-right: 1px solid var(--soon-border); + padding: var(--soon-space-md) 0; + overflow-y: auto; +} +.soon-admin-nav-item { + display: flex; + align-items: center; + gap: var(--soon-space-sm); + padding: 12px var(--soon-space-lg); + color: var(--soon-text); + cursor: pointer; + font-size: 14px; + border-left: 3px solid transparent; + transition: background 0.2s, border-color 0.2s, color 0.2s; +} +.soon-admin-nav-item img { + width: 20px; + height: 20px; + flex-shrink: 0; + opacity: 0.92; +} +.soon-admin-nav-item:hover { background: rgba(0, 150, 136, 0.08); } +.soon-admin-nav-item.is-active { + color: var(--soon-accent); + border-left-color: var(--soon-accent); + background: rgba(0, 150, 136, 0.12); +} +.soon-admin-nav-item.is-active img { opacity: 1; } + +.soon-admin-main { + flex: 1; + min-height: 0; + padding: var(--soon-space-lg); + overflow-x: hidden; + overflow-y: auto; +} + +.soon-admin-page { width: 100%; } + +.soon-admin-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--soon-space-md); + margin-bottom: var(--soon-space-lg); + flex-wrap: wrap; +} +.soon-admin-page-header__main h1 { + margin: 0; + font-size: 20px; + font-weight: 500; + color: var(--soon-text-strong); +} +.soon-admin-page-header__desc { + margin: var(--soon-space-xs) 0 0; + font-size: var(--soon-font-sm); + color: var(--soon-text-muted); +} +.soon-admin-page-header__actions { + display: flex; + gap: var(--soon-space-sm); + flex-wrap: wrap; +} + +.soon-admin-card { + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius); + padding: var(--soon-space-md); + margin-bottom: var(--soon-space-lg); +} +.soon-admin-card__title { + margin: 0 0 var(--soon-space-md); + font-size: var(--soon-font-md); + font-weight: 500; + color: var(--soon-text-strong); +} + +.soon-admin-filter-bar { + display: flex; + gap: var(--soon-space-sm); + margin-bottom: var(--soon-space-md); + align-items: center; + flex-wrap: wrap; +} + +.soon-admin-filter-field { + display: inline-flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.soon-admin-filter-field__label { + font-size: var(--soon-font-sm); + color: var(--soon-text-muted); + line-height: 1; + white-space: nowrap; +} + +.soon-admin-filter-bar .soon-input--sm { + max-width: 200px; + min-width: 120px; + margin: 0; +} + +.soon-admin-filter-bar .soon-input--date { + min-width: 148px; + max-width: 160px; +} + +.soon-admin-date-wrap { + position: relative; + display: inline-flex; + align-items: center; +} + +.soon-admin-date-wrap .soon-input--date { + position: relative; + padding-right: 32px; + background-color: var(--soon-bg-deep); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23eef1f4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='5' width='16' height='15' rx='2'/%3E%3Cpath d='M8 3v4M16 3v4M4 10h16'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 16px 16px; +} + +body.soon-admin-page .soon-admin-filter-bar select.soon-input { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23eef1f4' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 14px; + padding-right: 28px; +} + +body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webkit-calendar-picker-indicator { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + opacity: 0; + cursor: pointer; + background: transparent; + color: transparent; +} + +body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webkit-datetime-edit { + color: var(--soon-text-strong); +} + +body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +.soon-admin-stat-grid, +.soon-admin-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: var(--soon-space-md); + margin-bottom: var(--soon-space-xl); +} +.soon-admin-stat { + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius); + padding: var(--soon-space-lg); + animation: soon-fadeInUp 0.4s var(--soon-ease); + transition: border-color 0.2s; +} +.soon-admin-stat:hover { border-color: var(--soon-accent); } +.soon-admin-stat__label { font-size: 13px; color: var(--soon-text-muted); margin-bottom: var(--soon-space-sm); } +.soon-admin-stat__value { font-size: 28px; color: var(--soon-accent); font-weight: 600; } +.soon-admin-stat__hint { + margin-top: 8px; + font-size: 11px; + color: var(--soon-text-muted); + opacity: 0.85; +} +.soon-admin-stat--link { + display: block; + text-decoration: none; + color: inherit; + cursor: pointer; +} +.soon-admin-stat--link:hover { + border-color: var(--soon-accent); + transform: translateY(-1px); +} +.soon-admin-stat-grid--dash { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); +} + +.soon-admin-alert-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--soon-space-md); + margin-bottom: var(--soon-space-lg); + padding: 12px 16px; + border-radius: var(--soon-radius-md); + background: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.35); + font-size: var(--soon-font-sm); + color: #ffe082; +} +.soon-admin-alert-bar strong { color: #fff8e1; } + +.soon-admin-dash-grid { + display: grid; + grid-template-columns: 1fr 280px; + gap: var(--soon-space-lg); + align-items: start; +} +@media (max-width: 960px) { + .soon-admin-dash-grid { grid-template-columns: 1fr; } +} +.soon-admin-card__head-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--soon-space-sm); + margin-bottom: var(--soon-space-md); +} +.soon-admin-card__head-row .soon-admin-card__title { margin: 0; } +.soon-admin-card__link { + font-size: var(--soon-font-xs); + color: var(--soon-accent); + text-decoration: none; +} +.soon-admin-card__link:hover { text-decoration: underline; } + +.soon-admin-quick-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.soon-admin-quick-link { + display: block; + padding: 10px 12px; + border-radius: var(--soon-radius-sm); + background: var(--soon-bg-deep); + border: 1px solid var(--soon-border); + color: var(--soon-text); + font-size: var(--soon-font-sm); + text-decoration: none; + transition: border-color 0.2s, background 0.2s; +} +.soon-admin-quick-link:hover { + border-color: var(--soon-accent); + background: rgba(0, 150, 136, 0.08); +} + +.soon-admin-table { + width: 100%; + border-collapse: collapse; + background: var(--soon-bg-panel); + border-radius: var(--soon-radius); + overflow: hidden; +} +.soon-admin-table th, +.soon-admin-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--soon-border); + font-size: 13px; +} +.soon-admin-table th { + background: var(--soon-bg-elevated); + color: var(--soon-text-strong); + font-weight: 500; +} +.soon-admin-table tr:hover td { background: rgba(255, 255, 255, 0.02); } + +.soon-admin-drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 9000; +} +.soon-admin-drawer { + position: fixed; + top: 0; + right: 0; + width: 420px; + max-width: 100%; + height: 100%; + background: var(--soon-bg-panel); + border-left: 1px solid var(--soon-border); + z-index: 9001; + display: flex; + flex-direction: column; + animation: soon-fadeInUp 0.25s var(--soon-ease); +} +.soon-admin-drawer__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--soon-space-md) var(--soon-space-lg); + border-bottom: 1px solid var(--soon-border); +} +.soon-admin-drawer__header h3 { + margin: 0; + font-size: var(--soon-font-lg); + color: var(--soon-text-strong); +} +.soon-admin-drawer__close { + background: none; + border: none; + color: var(--soon-text-muted); + font-size: 22px; + cursor: pointer; + line-height: 1; +} +.soon-admin-drawer__body { + flex: 1; + overflow: auto; + padding: var(--soon-space-lg); +} +.soon-admin-detail-section { + margin-bottom: var(--soon-space-lg); +} +.soon-admin-detail-section h4 { + margin: 0 0 var(--soon-space-sm); + font-size: var(--soon-font-sm); + color: var(--soon-text-muted); + font-weight: 500; +} +.soon-admin-detail-row { + display: flex; + justify-content: space-between; + padding: 6px 0; + font-size: var(--soon-font-sm); + border-bottom: 1px solid var(--soon-border-subtle); +} +.soon-admin-detail-row__label { color: var(--soon-text-muted); } +.soon-admin-detail-row__value { color: var(--soon-text-strong); } + +.soon-admin-checklist { + list-style: none; + margin: 0 0 var(--soon-space-lg); + padding: 0; +} +.soon-admin-checklist li { + display: flex; + align-items: center; + gap: var(--soon-space-sm); + padding: 8px 0; + font-size: var(--soon-font-sm); +} +.soon-admin-checklist__dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} +.soon-admin-checklist__dot--ok { background: var(--soon-success); } +.soon-admin-checklist__dot--miss { background: var(--soon-danger); } + +.soon-admin-upload { + border: 2px dashed var(--soon-border); + border-radius: var(--soon-radius); + padding: var(--soon-space-xl); + text-align: center; + margin-bottom: var(--soon-space-lg); + transition: border-color 0.2s; +} +.soon-admin-upload.is-dragover { border-color: var(--soon-accent); } + +.soon-audit-timeline { + border-left: 2px solid var(--soon-border); + padding-left: var(--soon-space-lg); +} +.soon-audit-item { + position: relative; + padding-bottom: var(--soon-space-lg); + animation: soon-fadeInUp 0.3s var(--soon-ease); +} +.soon-audit-item::before { + content: ''; + position: absolute; + left: calc(-1 * var(--soon-space-lg) - 5px); + top: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--soon-accent); +} + +.panel { display: none; } +.panel.is-visible { display: block; } + +.soon-admin-setting-hint { + font-size: var(--soon-font-xs); + color: var(--soon-text-muted); +} + +.soon-admin-table-wrap { + overflow-x: auto; + margin-bottom: var(--soon-space-lg); + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.18) transparent; +} +.soon-admin-table-wrap::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.soon-admin-table-wrap::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.16); + border-radius: 4px; +} +.soon-admin-main, +.soon-admin-sidebar { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.18) transparent; +} +.soon-admin-main::-webkit-scrollbar, +.soon-admin-sidebar::-webkit-scrollbar { + width: 8px; +} +.soon-admin-main::-webkit-scrollbar-thumb, +.soon-admin-sidebar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.16); + border-radius: 4px; +} +.soon-admin-form { padding: var(--soon-space-md); } +.soon-admin-form--plan { + padding: var(--soon-space-lg); +} +.soon-admin-form--plan .soon-admin-form-panel:last-of-type { + margin-bottom: 0; +} +.soon-admin-form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 var(--soon-space-md); +} +@media (max-width: 560px) { + .soon-admin-form-grid { grid-template-columns: 1fr; } +} +.soon-admin-form-checks { + display: flex; + flex-wrap: wrap; + gap: var(--soon-space-sm) var(--soon-space-lg); + margin-top: var(--soon-space-sm); + padding-top: var(--soon-space-sm); + border-top: 1px solid var(--soon-border); +} +.soon-admin-check { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: var(--soon-font-sm); + color: var(--soon-text); + cursor: pointer; + user-select: none; +} +.soon-admin-check input { accent-color: var(--soon-accent); } +.soon-admin-feature-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: var(--soon-space-sm); +} +.soon-admin-feature-row { + display: flex; + align-items: center; + gap: 8px; +} +.soon-admin-feature-row .soon-admin-feature-input { + flex: 1; + min-width: 0; +} +.soon-admin-feature-del { + flex-shrink: 0; + min-width: 32px; + padding: 0; + font-size: 16px; + line-height: 1; +} +.soon-admin-form-hint--tight { + margin-top: var(--soon-space-sm); + margin-bottom: 0; +} +.soon-admin-hint { + margin-bottom: var(--soon-space-md); + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); +} +.soon-admin-hint summary { cursor: pointer; color: var(--soon-text); } +.soon-admin-hint p { margin-top: var(--soon-space-sm); } +.soon-admin-upload__label { color: var(--soon-accent); cursor: pointer; } +.soon-admin-filters { display: flex; gap: var(--soon-space-sm); margin-bottom: var(--soon-space-md); flex-wrap: wrap; } +.soon-admin-filters .soon-input--sm { width: auto; min-width: 120px; } + +.soon-input--textarea { + height: auto; + min-height: 88px; + padding: 8px 12px; + resize: vertical; + line-height: 1.5; + width: 100%; + box-sizing: border-box; +} +.soon-admin-logout-btn { margin-left: var(--soon-space-sm); } + +.soon-audit-item__title { color: var(--soon-text-strong); font-size: var(--soon-font-md); } +.soon-audit-item__meta { color: var(--soon-text-muted); font-size: var(--soon-font-xs); margin-top: var(--soon-space-xs); } +.soon-audit-item__context { color: var(--soon-text-muted); font-size: var(--soon-font-xs); margin-top: var(--soon-space-xs); opacity: 0.85; } +.soon-audit-item__details { + margin-top: var(--soon-space-sm); + font-size: var(--soon-font-xs); +} +.soon-audit-item__details summary { + cursor: pointer; + color: var(--soon-accent); + user-select: none; +} +.soon-audit-item__details[open] summary { margin-bottom: var(--soon-space-xs); } +.soon-admin-code { + margin: var(--soon-space-xs) 0 0; + padding: var(--soon-space-sm); + background: var(--soon-bg-deep); + border-radius: var(--soon-radius-sm); + font-size: var(--soon-font-xs); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; +} + +.soon-admin-empty-row td { text-align: center; color: var(--soon-text-muted); padding: var(--soon-space-xl); } + +.soon-admin-pager { + margin-top: var(--soon-space-md); + display: flex; + justify-content: space-between; + gap: var(--soon-space-md); + align-items: center; + flex-wrap: wrap; + padding: var(--soon-space-sm) 0; + border-top: 1px solid var(--soon-border); +} +.soon-admin-pager__left, +.soon-admin-pager__right { + display: flex; + align-items: center; + gap: var(--soon-space-sm); + flex-wrap: wrap; +} +.soon-admin-pager__info { + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); +} +.soon-admin-pager__size-label, +.soon-admin-pager__jump-label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: var(--soon-font-xs); + color: var(--soon-text-muted); +} +.soon-pager-jump { width: 56px; text-align: center; } +.soon-admin-pager--empty { justify-content: flex-start; } + +.soon-admin-tabs__nav { + display: flex; + gap: 4px; + padding: 4px; + margin-bottom: var(--soon-space-md); + background: var(--soon-bg-deep); + border-radius: var(--soon-radius-md); + border: 1px solid var(--soon-border); +} +.soon-admin-tabs__tab { + flex: 1; + min-width: 0; + padding: 10px 16px; + border: none; + border-radius: var(--soon-radius-sm); + background: transparent; + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); + font-weight: 500; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} +.soon-admin-tabs__tab:hover { color: var(--soon-text); background: rgba(255,255,255,0.04); } +.soon-admin-tabs__tab.is-active { + color: var(--soon-text-strong); + background: rgba(0, 150, 136, 0.15); + box-shadow: inset 0 0 0 1px rgba(0, 150, 136, 0.25); +} +.soon-admin-tabs__panel { display: none; } +.soon-admin-tabs__panel.is-active { display: block; } + +.soon-admin-form-panel { + background: var(--soon-bg-elevated); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius-md); + padding: var(--soon-space-lg); + margin-bottom: var(--soon-space-md); +} +.soon-admin-form-panel__title { + margin: 0 0 var(--soon-space-md); + font-size: var(--soon-font-md); + font-weight: 600; + color: var(--soon-text-strong); +} +.soon-admin-form-panel__body .soon-form-field:last-child { margin-bottom: 0; } +.soon-admin-form__actions { + display: flex; + justify-content: flex-end; + gap: var(--soon-space-sm); + margin-top: var(--soon-space-lg); + padding-top: var(--soon-space-md); + border-top: 1px solid var(--soon-border); +} + +.soon-admin-filter-bar { + display: flex; + flex-wrap: wrap; + gap: var(--soon-space-sm); + align-items: flex-end; + margin-bottom: var(--soon-space-md); + padding: var(--soon-space-md); + background: var(--soon-bg-elevated); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius-md); +} +.soon-admin-filter-bar__fields { + display: flex; + flex-wrap: wrap; + gap: var(--soon-space-sm); + flex: 1; + align-items: flex-end; +} +.soon-admin-filter-bar__actions { + display: flex; + gap: var(--soon-space-sm); + flex-shrink: 0; +} + +.soon-admin-header__env { + margin-left: var(--soon-space-sm); + padding: 2px 8px; + font-size: 11px; + font-weight: 600; + color: #ffe082; + background: rgba(255, 193, 7, 0.12); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 4px; +} +.soon-admin-header__email { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.soon-admin-key-form-row { + display: grid; + grid-template-columns: 140px 1fr auto; + gap: var(--soon-space-sm); + align-items: start; + padding: var(--soon-space-md) 0; + border-bottom: 1px solid var(--soon-border); +} +.soon-admin-key-form-row:last-child { border-bottom: none; } +.soon-admin-key-form-row__label { + font-size: var(--soon-font-sm); + color: var(--soon-text); + padding-top: 8px; +} +.soon-admin-key-form-row__status { + font-size: var(--soon-font-xs); + margin-top: 4px; +} +.soon-admin-key-form-row__status--ok { color: var(--soon-accent); } +.soon-admin-key-form-row__status--miss { color: var(--soon-danger); } + +.soon-admin-btn-group { + display: flex; + gap: var(--soon-space-xs); + flex-wrap: wrap; +} + +.soon-admin-row-actions { + display: inline-flex; + align-items: center; + gap: 4px; + flex-wrap: nowrap; + white-space: nowrap; +} +.soon-admin-table__actions { + overflow: visible; + min-width: 200px; +} +.soon-admin-action-menu { + position: relative; + display: inline-block; +} +.soon-admin-action-menu__panel { + display: none; + position: absolute; + right: 0; + top: calc(100% + 4px); + min-width: 120px; + padding: 4px; + background: var(--soon-bg-panel); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius-sm); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); + z-index: 50; +} +.soon-admin-action-menu__panel--floating { + position: fixed; + right: auto; + top: auto; + z-index: 1200; +} +.soon-admin-action-menu.is-open .soon-admin-action-menu__panel { + display: block; +} +.soon-admin-action-menu--more .soon-admin-action-menu__toggle { + min-width: 52px; +} +.soon-admin-action-menu__item { + display: block; + width: 100%; + padding: 8px 12px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--soon-text); + font-size: var(--soon-font-sm); + text-align: left; + cursor: pointer; +} +.soon-admin-action-menu__item:hover:not(:disabled) { + background: rgba(0, 150, 136, 0.12); +} +.soon-admin-action-menu__item:disabled { + opacity: 0.45; + cursor: not-allowed; +} +.soon-admin-action-menu__item.soon-admin-action-menu__item--danger { + color: var(--soon-danger); +} + +.soon-admin-list-error { + margin-top: var(--soon-space-md); + padding: var(--soon-space-lg); + text-align: center; + background: var(--soon-bg-elevated); + border: 1px solid var(--soon-border); + border-radius: var(--soon-radius-md); +} +.soon-admin-list-error p { + margin: 0 0 var(--soon-space-md); + color: var(--soon-text-muted); + font-size: var(--soon-font-sm); +} + +.soon-btn--xs { + height: 28px; + padding: 0 8px; + font-size: var(--soon-font-xs); +} + +.soon-btn--danger { + color: var(--soon-danger) !important; + border-color: rgba(229, 115, 115, 0.45) !important; +} + +.soon-btn--danger:hover { + color: #fff !important; + background: var(--soon-danger) !important; + border-color: var(--soon-danger) !important; +} + +.soon-admin-form-hint { + margin: 0 0 var(--soon-space-md); + font-size: var(--soon-font-xs); + color: var(--soon-text-muted); + line-height: 1.5; +} + +.soon-admin-detail-empty { + margin: 0; + font-size: var(--soon-font-sm); + color: var(--soon-text-muted); +} + +.soon-admin-drawer-actions { + display: flex; + flex-wrap: wrap; + gap: var(--soon-space-sm); + margin-bottom: var(--soon-space-lg); + padding-bottom: var(--soon-space-md); + border-bottom: 1px solid var(--soon-border); +} + +@media (max-width: 768px) { + .soon-admin-sidebar { width: 56px; } + .soon-admin-nav-item span { display: none; } + .soon-admin-nav-item { justify-content: center; padding: 12px; } +} + +.soon-admin-empty-icon { + width: 72px; + height: 72px; + margin-bottom: var(--soon-space-md); + display: block; + flex-shrink: 0; + opacity: 0.92; + pointer-events: none; + user-select: none; +} + +body.soon-admin-page .soon-empty--inline { + min-height: 140px; + padding: var(--soon-space-lg) var(--soon-space-md); +} + +body.soon-admin-page .soon-input-wrap .soon-input-icon { + opacity: 0.88; +} + +body.soon-admin-page .soon-input-wrap .soon-input-icon svg { + stroke: #ffffff; +} + +body.soon-admin-page .layui-layer.soon-layer .layui-layer-ico, +body.soon-admin-page .layui-layer-soon-layer .layui-layer-ico { + filter: brightness(0) invert(1); + opacity: 0.92; +} diff --git a/frontend-web/pages/admin/assets/js/components/filter.js b/frontend-web/pages/admin/assets/js/components/filter.js new file mode 100644 index 0000000..c1a0e5f --- /dev/null +++ b/frontend-web/pages/admin/assets/js/components/filter.js @@ -0,0 +1,28 @@ +(function () { + 'use strict'; + + var I = function () { return window.AdminI18n; }; + + function bar(innerHtml, actionsHtml) { + return '
    ' + + '
    ' + innerHtml + '
    ' + + (actionsHtml ? '
    ' + actionsHtml + '
    ' : '') + + '
    '; + } + + function field(label, inner) { + return '
    ' + + I().esc(label) + '' + inner + '
    '; + } + + function searchInput(id, value, placeholder) { + return ''; + } + + function select(id, optionsHtml) { + return ''; + } + + window.AdminFilter = { bar: bar, field: field, searchInput: searchInput, select: select }; +})(); diff --git a/frontend-web/pages/admin/assets/js/components/form.js b/frontend-web/pages/admin/assets/js/components/form.js new file mode 100644 index 0000000..1012250 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/components/form.js @@ -0,0 +1,53 @@ +(function () { + 'use strict'; + + var I = function () { return window.AdminI18n; }; + + function field(label, inner, hint) { + var html = '
    ' + inner; + if (hint) { + html += '

    ' + I().esc(hint) + '

    '; + } + return html + '
    '; + } + + function input(id, opts) { + opts = opts || {}; + var type = opts.type || 'text'; + var cls = 'soon-input soon-input--sm' + (opts.className ? ' ' + opts.className : ''); + var attrs = 'id="' + id + '" class="' + cls + '"'; + if (opts.value != null) attrs += ' value="' + I().esc(opts.value) + '"'; + if (opts.placeholder) attrs += ' placeholder="' + I().esc(opts.placeholder) + '"'; + if (opts.disabled) attrs += ' disabled'; + if (opts.autocomplete) attrs += ' autocomplete="' + I().esc(opts.autocomplete) + '"'; + if (type === 'textarea') { + return ''; + } + return ''; + } + + function select(id, optionsHtml) { + return ''; + } + + function actions(primaryLabel, primaryId, secondaryHtml) { + return '
    ' + + (secondaryHtml || '') + + '
    '; + } + + function panel(title, bodyHtml) { + return '

    ' + I().esc(title) + + '

    ' + bodyHtml + '
    '; + } + + window.AdminForm = { + field: field, + input: input, + select: select, + actions: actions, + panel: panel, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/components/menu.js b/frontend-web/pages/admin/assets/js/components/menu.js new file mode 100644 index 0000000..9c4c490 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/components/menu.js @@ -0,0 +1,168 @@ +(function () { + 'use strict'; + + var I = function () { return window.AdminI18n; }; + + function itemDataset(it) { + if (!it.dataset) return ''; + return Object.keys(it.dataset).map(function (k) { + return ' data-' + k + '="' + I().esc(it.dataset[k]) + '"'; + }).join(''); + } + + function menuItemBtn(it) { + if (it.disabled) { + return ''; + } + var cls = 'soon-admin-action-menu__item' + (it.className ? ' ' + it.className : ''); + return ''; + } + + function inlineBtn(it) { + if (it.disabled) { + return ''; + } + return ''; + } + + function positionFloatingPanel(menu) { + var toggle = menu.querySelector('.soon-admin-action-menu__toggle'); + var panel = menu.querySelector('.soon-admin-action-menu__panel'); + if (!toggle || !panel) return; + panel.classList.add('soon-admin-action-menu__panel--floating'); + panel.style.display = 'block'; + panel.style.visibility = 'hidden'; + var r = toggle.getBoundingClientRect(); + var panelW = panel.offsetWidth; + var panelH = panel.offsetHeight; + var gap = 4; + var left = r.right - panelW; + if (left < 8) left = 8; + if (left + panelW > window.innerWidth - 8) left = window.innerWidth - panelW - 8; + var top = r.bottom + gap; + if (top + panelH > window.innerHeight - 8) { + top = Math.max(8, r.top - panelH - gap); + } + panel.style.left = left + 'px'; + panel.style.top = top + 'px'; + panel.style.visibility = ''; + } + + function actionMenu(menuId, items) { + var btns = items.map(menuItemBtn).join(''); + return '
    ' + + '' + + '
    ' + btns + '
    '; + } + + function rowActions(menuId, inlineItems, moreItems) { + var inline = (inlineItems || []).map(inlineBtn).join(''); + var more = (moreItems || []).map(menuItemBtn).join(''); + if (!more) { + return '
    ' + inline + '
    '; + } + return '
    ' + inline + + '
    ' + + '' + + '
    ' + more + '
    '; + } + + function closeAll() { + document.querySelectorAll('.soon-admin-action-menu.is-open').forEach(function (m) { + m.classList.remove('is-open'); + resetPanelStyle(m.querySelector('.soon-admin-action-menu__panel')); + }); + } + + function bindActionButtons(container, handler, selector) { + container.querySelectorAll(selector).forEach(function (btn) { + if (btn.disabled) return; + btn.onclick = function (e) { + e.stopPropagation(); + closeAll(); + if (typeof handler === 'function') handler(btn.dataset.action, btn); + }; + }); + } + + function resetPanelStyle(panel) { + if (!panel) return; + panel.style.display = ''; + panel.style.left = ''; + panel.style.top = ''; + panel.style.visibility = ''; + } + + function bindHoverMore(container) { + container.querySelectorAll('.soon-admin-action-menu--more').forEach(function (menu) { + var hideTimer = null; + var panel = menu.querySelector('.soon-admin-action-menu__panel'); + function openMenu() { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + closeAll(); + positionFloatingPanel(menu); + menu.classList.add('is-open'); + } + function scheduleClose() { + hideTimer = setTimeout(function () { + menu.classList.remove('is-open'); + resetPanelStyle(panel); + }, 180); + } + function cancelClose() { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + } + menu.addEventListener('mouseenter', openMenu); + menu.addEventListener('mouseleave', scheduleClose); + if (panel) { + panel.addEventListener('mouseenter', cancelClose); + panel.addEventListener('mouseleave', scheduleClose); + } + }); + } + + function bind(container, handler) { + if (!container) return; + bindActionButtons(container, handler, '.soon-admin-row-actions__btn[data-action]'); + container.querySelectorAll('.soon-admin-action-menu__toggle').forEach(function (btn) { + var menu = btn.closest('.soon-admin-action-menu'); + if (menu && menu.classList.contains('soon-admin-action-menu--more')) return; + btn.onclick = function (e) { + e.stopPropagation(); + var wasOpen = menu.classList.contains('is-open'); + closeAll(); + if (!wasOpen) { + positionFloatingPanel(menu); + menu.classList.add('is-open'); + } + }; + }); + bindActionButtons(container, handler, '.soon-admin-action-menu__item[data-action]'); + bindHoverMore(container); + } + + if (!window._soonAdminMenuDocBound) { + window._soonAdminMenuDocBound = true; + document.addEventListener('click', closeAll); + window.addEventListener('scroll', closeAll, true); + window.addEventListener('resize', closeAll); + } + + window.AdminMenu = { + actionMenu: actionMenu, + rowActions: rowActions, + bind: bind, + closeAll: closeAll, + positionFloatingPanel: positionFloatingPanel, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/components/pager.js b/frontend-web/pages/admin/assets/js/components/pager.js new file mode 100644 index 0000000..18e1a36 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/components/pager.js @@ -0,0 +1,83 @@ +(function () { + 'use strict'; + + var SIZE_OPTIONS = [10, 20, 50, 100]; + + function pagesCount(total, size) { + return Math.max(1, Math.ceil((total || 0) / (size || 20))); + } + + function html(opts) { + opts = opts || {}; + var page = opts.page || 1; + var size = opts.size || 20; + var total = opts.total || 0; + var pages = pagesCount(total, size); + var sizes = opts.sizeOptions || SIZE_OPTIONS; + if (total === 0) { + return '
    共 0 条
    '; + } + var sizeOpts = sizes.map(function (n) { + return ''; + }).join(''); + return '
    ' + + '
    ' + + '' + + '' + + '' + page + ' / ' + pages + ' 页,共 ' + total + ' 条' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    '; + } + + function bind(container, state, onChange) { + if (!container) return; + var page = state.page || 1; + var size = state.size || 20; + var total = state.total || 0; + var pages = pagesCount(total, size); + + function go(p) { + p = Math.max(1, Math.min(pages, p)); + if (p !== page) onChange({ page: p }); + } + + var first = container.querySelector('.soon-pager-first'); + var prev = container.querySelector('.soon-pager-prev'); + var next = container.querySelector('.soon-pager-next'); + var last = container.querySelector('.soon-pager-last'); + var sizeSel = container.querySelector('.soon-pager-size'); + var jump = container.querySelector('.soon-pager-jump'); + var goBtn = container.querySelector('.soon-pager-go'); + + if (first && !first.disabled) first.onclick = function () { go(1); }; + if (prev && !prev.disabled) prev.onclick = function () { go(page - 1); }; + if (next && !next.disabled) next.onclick = function () { go(page + 1); }; + if (last && !last.disabled) last.onclick = function () { go(pages); }; + if (sizeSel) { + sizeSel.onchange = function () { + onChange({ page: 1, size: parseInt(sizeSel.value, 10) || size }); + }; + } + if (goBtn && jump) { + goBtn.onclick = function () { + go(parseInt(jump.value, 10) || page); + }; + jump.onkeydown = function (e) { + if (e.key === 'Enter') go(parseInt(jump.value, 10) || page); + }; + } + } + + window.AdminPager = { html: html, bind: bind, pagesCount: pagesCount }; +})(); diff --git a/frontend-web/pages/admin/assets/js/components/shell.js b/frontend-web/pages/admin/assets/js/components/shell.js new file mode 100644 index 0000000..aefccaa --- /dev/null +++ b/frontend-web/pages/admin/assets/js/components/shell.js @@ -0,0 +1,40 @@ +(function () { + 'use strict'; + + var I = function () { return window.AdminI18n; }; + + function headerHtml(opts) { + opts = opts || {}; + return '
    ' + + '
    ' + + '' + + '' + I().esc(opts.title || 'SoonDesign Admin') + '' + + (opts.env ? '' + I().esc(opts.env) + '' : '') + + '
    ' + + '
    ' + + (opts.designEntry !== false + ? '进入设计端' + : '') + + '' + + '' + + '
    '; + } + + function mountHeader(container, opts) { + if (!container) return; + container.innerHTML = headerHtml(opts); + } + + function bindHeader(handlers) { + handlers = handlers || {}; + var logout = document.getElementById('adminLogout'); + if (logout && handlers.onLogout) logout.onclick = handlers.onLogout; + } + + window.AdminShell = { + headerHtml: headerHtml, + mountHeader: mountHeader, + bindHeader: bindHeader, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/components/tabs.js b/frontend-web/pages/admin/assets/js/components/tabs.js new file mode 100644 index 0000000..5eed06d --- /dev/null +++ b/frontend-web/pages/admin/assets/js/components/tabs.js @@ -0,0 +1,40 @@ +(function () { + 'use strict'; + + var I = function () { return window.AdminI18n; }; + + function html(id, tabs, panels, activeId) { + activeId = activeId || (tabs[0] && tabs[0].id); + var nav = tabs.map(function (t) { + var cls = t.id === activeId ? ' soon-admin-tabs__tab is-active' : ' soon-admin-tabs__tab'; + return ''; + }).join(''); + var body = tabs.map(function (t) { + var vis = t.id === activeId ? ' is-active' : ''; + return '
    ' + + (panels[t.id] || '') + '
    '; + }).join(''); + return '
    ' + + '' + + '
    ' + body + '
    '; + } + + function bind(root, onChange) { + if (!root) return; + root.querySelectorAll('.soon-admin-tabs__tab').forEach(function (btn) { + btn.onclick = function () { + var tabId = btn.dataset.tab; + root.querySelectorAll('.soon-admin-tabs__tab').forEach(function (b) { + b.classList.toggle('is-active', b.dataset.tab === tabId); + }); + root.querySelectorAll('.soon-admin-tabs__panel').forEach(function (p) { + p.classList.toggle('is-active', p.dataset.panel === tabId); + }); + if (typeof onChange === 'function') onChange(tabId); + }; + }); + } + + window.AdminTabs = { html: html, bind: bind }; +})(); diff --git a/frontend-web/pages/admin/assets/js/core/api.js b/frontend-web/pages/admin/assets/js/core/api.js new file mode 100644 index 0000000..87215a2 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/core/api.js @@ -0,0 +1,265 @@ +(function () { + 'use strict'; + + var TOKEN_KEY = 'soon_access'; + var REFRESH_KEY = 'soon_refresh'; + var refreshPromise = null; + var scopeControllers = {}; + + function getAdminBase() { + var cfg = window.SOON_DEPLOY_CONFIG || {}; + return (cfg.api_admin_base || '').replace(/\/+$/, ''); + } + + function getV1Base() { + var cfg = window.SOON_DEPLOY_CONFIG || {}; + return (cfg.api_v1_base || '').replace(/\/+$/, ''); + } + + function getToken() { + try { + return localStorage.getItem(TOKEN_KEY) || ''; + } catch (e) { + return ''; + } + } + + function getRefreshToken() { + try { + return localStorage.getItem(REFRESH_KEY) || ''; + } catch (e) { + return ''; + } + } + + function clearToken() { + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + } catch (e) { /* ignore */ } + } + + function loginRedirectUrl() { + var hash = location.hash || ''; + var redirect = 'index.html' + hash; + return 'login.html?redirect=' + encodeURIComponent(redirect); + } + + function goLogin() { + window.location.href = loginRedirectUrl(); + } + + function apiUrl(path) { + var p = String(path || '').replace(/^\//, ''); + return getAdminBase() + '/' + p; + } + + function defaultScope() { + if (window.AdminRouter && typeof AdminRouter.currentTab === 'function') { + return AdminRouter.currentTab() || 'app'; + } + return 'app'; + } + + function abortScope(scope) { + if (!scope) return; + var ctrl = scopeControllers[scope]; + if (!ctrl) return; + ctrl.abort(); + delete scopeControllers[scope]; + } + + function attachScope(scope) { + abortScope(scope); + var ctrl = new AbortController(); + scopeControllers[scope] = ctrl; + return ctrl.signal; + } + + function isAbortError(err) { + return err && (err.name === 'AbortError' || err.code === 20); + } + + function refreshAccessToken() { + if (refreshPromise) return refreshPromise; + var refresh = getRefreshToken(); + if (!refresh) { + return Promise.reject(new Error('no_refresh_token')); + } + refreshPromise = window.fetch(getV1Base() + '/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: refresh }), + }).then(function (r) { + return r.json().then(function (j) { + if (!r.ok || !j.ok || !j.data || !j.data.access_token) { + throw new Error(j.message || 'refresh_failed'); + } + localStorage.setItem(TOKEN_KEY, j.data.access_token); + if (j.data.refresh_token) { + localStorage.setItem(REFRESH_KEY, j.data.refresh_token); + } + return j.data.access_token; + }); + }).finally(function () { + refreshPromise = null; + }); + return refreshPromise; + } + + function sessionExpired() { + clearToken(); + if (typeof soonToast === 'function') soonToast('登录已过期,请重新登录', 'warn'); + setTimeout(goLogin, 600); + } + + async function readJsonResponse(r, signal) { + if (signal && signal.aborted) return null; + var j; + try { + j = await r.json(); + } catch (e) { + if (isAbortError(e)) return null; + if (typeof soonToast === 'function') soonToast('响应解析失败', 'error'); + return null; + } + if (signal && signal.aborted) return null; + return j; + } + + async function httpRequest(url, opts) { + opts = opts || {}; + var signal = opts.signal; + var headers = Object.assign({ + Authorization: 'Bearer ' + getToken(), + 'Content-Type': 'application/json', + }, opts.headers || {}); + var reqOpts = Object.assign({}, opts, { headers: headers, signal: signal }); + delete reqOpts.scope; + delete reqOpts.retry401; + + try { + return await window.fetch(url, reqOpts); + } catch (e) { + if (isAbortError(e)) return null; + throw e; + } + } + + async function requestWithAuth(url, opts) { + opts = opts || {}; + var signal = opts.signal; + var r = await httpRequest(url, opts); + if (!r) return null; + + if (r.status === 401 && !opts.retry401) { + try { + await refreshAccessToken(); + } catch (e) { + sessionExpired(); + return null; + } + var retryOpts = Object.assign({}, opts, { retry401: true }); + retryOpts.headers = Object.assign({}, opts.headers || {}, { + Authorization: 'Bearer ' + getToken(), + }); + r = await httpRequest(url, retryOpts); + if (!r) return null; + if (r.status === 401) { + sessionExpired(); + return null; + } + } + + if (r.status === 403) { + var forbidden = await readJsonResponse(r, signal); + if (forbidden && typeof soonToast === 'function') { + soonToast(forbidden.message || '无权限执行此操作', 'warn'); + } + return null; + } + + return r; + } + + async function fetch(path, opts) { + opts = opts || {}; + var scope = opts.scope || defaultScope(); + var signal = opts.signal || attachScope(scope); + var r; + try { + r = await requestWithAuth(apiUrl(path), Object.assign({}, opts, { signal: signal })); + } catch (e) { + if (isAbortError(e)) return null; + if (typeof soonToast === 'function') soonToast('网络错误,请稍后重试', 'error'); + return null; + } + if (!r || (signal && signal.aborted)) return null; + + var j = await readJsonResponse(r, signal); + if (!j || (signal && signal.aborted)) return null; + if (!j.ok) { + if (typeof soonToast === 'function') soonToast(j.message || '操作失败', 'error'); + return null; + } + return j; + } + + async function get(path, opts) { + return fetch(path, Object.assign({}, opts || {}, { method: 'GET' })); + } + + async function postJson(path, body, opts) { + return fetch(path, Object.assign({}, opts || {}, { + method: 'POST', + body: JSON.stringify(body || {}), + })); + } + + async function putJson(path, body, opts) { + return fetch(path, Object.assign({}, opts || {}, { + method: 'PUT', + body: JSON.stringify(body || {}), + })); + } + + async function del(path, opts) { + return fetch(path, Object.assign({}, opts || {}, { method: 'DELETE' })); + } + + async function fetchMe() { + if (!getToken()) return null; + var signal = attachScope('auth'); + var r; + try { + r = await requestWithAuth(getV1Base() + '/auth/me', { + method: 'GET', + signal: signal, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (e) { + if (isAbortError(e)) return null; + return null; + } + if (!r || signal.aborted) return null; + var j = await readJsonResponse(r, signal); + if (!j || signal.aborted) return null; + if (!j.ok || !j.data) return null; + return j.data; + } + + window.AdminApi = { + getToken: getToken, + clearToken: clearToken, + goLogin: goLogin, + getV1Base: getV1Base, + fetch: fetch, + get: get, + postJson: postJson, + putJson: putJson, + del: del, + fetchMe: fetchMe, + abortScope: abortScope, + refreshAccessToken: refreshAccessToken, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/core/i18n.js b/frontend-web/pages/admin/assets/js/core/i18n.js new file mode 100644 index 0000000..5e2eaac --- /dev/null +++ b/frontend-web/pages/admin/assets/js/core/i18n.js @@ -0,0 +1,124 @@ +(function () { + 'use strict'; + + var STATUS = { + active: '正常', + disabled: '已停用', + pending: '待支付', + paid: '已支付', + cancelled: '已取消', + refunded: '已退款', + expired: '已过期', + }; + + var REFUND_STATUS = { + none: '无申请', + pending: '待审核', + approved: '已通过', + rejected: '已拒绝', + }; + + var ROLE = { + user: '普通用户', + admin: '管理员', + }; + + var CHANNEL = { + alipay: '支付宝', + wechat: '微信支付', + }; + + var ACTION = { + 'stats.summary': '查看概览', + 'users.list': '浏览用户列表', + 'users.create': '创建用户', + 'users.update': '编辑用户', + 'users.delete': '删除用户', + 'users.detail': '查看用户详情', + 'users.setPassword': '重置用户密码', + 'users.setSubscription': '设置用户会员', + 'users.setStatus': '变更用户状态', + 'orders.list': '浏览订单列表', + 'orders.detail': '查看订单详情', + 'orders.cancel': '取消订单', + 'orders.refundApprove': '通过退款', + 'orders.refundReject': '拒绝退款', + 'plans.list': '浏览套餐列表', + 'plans.upsert': '编辑套餐', + 'settings.list': '浏览系统设置', + 'settings.set': '修改系统设置', + 'settings.set_batch': '批量修改系统设置', + 'payment.key.upload': '上传支付密钥', + 'payment.key.status': '查看支付密钥', + }; + + var STATUS_BADGE = { + active: 'soon-badge--active', + disabled: 'soon-badge--disabled', + pending: 'soon-badge--pending', + paid: 'soon-badge--paid', + cancelled: 'soon-badge--disabled', + refunded: 'soon-badge--pending', + expired: 'soon-badge--disabled', + }; + + var REFUND_BADGE = { + none: 'soon-badge--disabled', + pending: 'soon-badge--pending', + approved: 'soon-badge--paid', + rejected: 'soon-badge--disabled', + }; + + function t(map, v) { + if (v == null || v === '') return '-'; + return map[v] || String(v); + } + + function esc(s) { + if (typeof soonEscapeHtml === 'function') return soonEscapeHtml(s); + if (s == null) return ''; + return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); + } + + function badgeStatus(v) { + var cls = STATUS_BADGE[v] || 'soon-badge--pending'; + return '' + esc(t(STATUS, v)) + ''; + } + + function badgeRefundStatus(v) { + var key = v || 'none'; + var cls = REFUND_BADGE[key] || 'soon-badge--disabled'; + return '' + esc(t(REFUND_STATUS, key)) + ''; + } + + function formatCents(n) { + var c = parseInt(n, 10) || 0; + return '¥' + (c / 100).toFixed(2); + } + + function formatBytes(n) { + var b = parseInt(n, 10) || 0; + if (b < 1024) return b + ' B'; + if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB'; + return (b / (1024 * 1024)).toFixed(2) + ' MB'; + } + + function formatDateTime(s) { + if (!s) return '-'; + return String(s).replace('T', ' ').slice(0, 19); + } + + window.AdminI18n = { + tStatus: function (v) { return t(STATUS, v); }, + tRole: function (v) { return t(ROLE, v); }, + tChannel: function (v) { return t(CHANNEL, v); }, + tAction: function (v) { return t(ACTION, v); }, + badgeStatus: badgeStatus, + badgeRefundStatus: badgeRefundStatus, + tRefundStatus: function (v) { return t(REFUND_STATUS, v || 'none'); }, + formatCents: formatCents, + formatBytes: formatBytes, + formatDateTime: formatDateTime, + esc: esc, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/core/permissions.js b/frontend-web/pages/admin/assets/js/core/permissions.js new file mode 100644 index 0000000..c3fc493 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/core/permissions.js @@ -0,0 +1,44 @@ +(function () { + 'use strict'; + + var FULL_ONLY = { settings: true, audits: true, payment: true }; + + function normalizeLevel(level) { + return level === 'ops' ? 'ops' : 'full'; + } + + function canAccessTab(tab, level) { + level = normalizeLevel(level); + if (level === 'full') return true; + return !FULL_ONLY[tab]; + } + + function defaultTab(level) { + return 'dashboard'; + } + + function applyNav(level) { + level = normalizeLevel(level); + document.querySelectorAll('.soon-admin-nav-item').forEach(function (el) { + var req = el.getAttribute('data-require') || 'ops'; + var show = req !== 'full' || level === 'full'; + el.style.display = show ? '' : 'none'; + }); + } + + function filterQuickLinks(links, level) { + level = normalizeLevel(level); + return links.filter(function (l) { + var tab = (l.href || '').replace(/^#\//, '').split(/[/?]/)[0]; + return canAccessTab(tab, level); + }); + } + + window.AdminPermissions = { + canAccessTab: canAccessTab, + defaultTab: defaultTab, + applyNav: applyNav, + filterQuickLinks: filterQuickLinks, + normalizeLevel: normalizeLevel, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/core/router.js b/frontend-web/pages/admin/assets/js/core/router.js new file mode 100644 index 0000000..6cc3d7d --- /dev/null +++ b/frontend-web/pages/admin/assets/js/core/router.js @@ -0,0 +1,297 @@ +(function () { + 'use strict'; + + var State = function () { return window.AdminState; }; + var Perms = function () { return window.AdminPermissions; }; + + var ROUTES = { + '': 'dashboard', + '/': 'dashboard', + '/dashboard': 'dashboard', + '/users': 'users', + '/orders': 'orders', + '/plans': 'plans', + '/settings': 'settings', + '/audits': 'audits', + '/payment': 'payment', + }; + + var TAB_TO_PATH = { + dashboard: '/dashboard', + users: '/users', + orders: '/orders', + plans: '/plans', + settings: '/settings', + audits: '/audits', + payment: '/payment', + }; + + var DETAIL_TABS = { users: true, orders: true }; + + var FILTER_DEFS = { + users: { + stateKey: 'users', + defaults: { page: 1, size: 20, q: '' }, + fields: { + q: 'string', + page: 'int', + size: 'int', + }, + }, + orders: { + stateKey: 'orders', + defaults: { page: 1, size: 20, q: '', status: '', refund_status: '', channel: '', from: '', to: '' }, + fields: { + q: 'string', + page: 'int', + size: 'int', + status: 'string', + refund_status: 'string', + channel: 'string', + from: 'string', + to: 'string', + }, + }, + audits: { + stateKey: 'audits', + defaults: { page: 1, size: 50, q: '', from: '', to: '', action: '' }, + fields: { + q: 'string', + page: 'int', + size: 'int', + from: 'string', + to: 'string', + action: 'string', + }, + }, + settings: { + stateKey: 'settings', + defaults: { tab: 'site', q: '' }, + fields: { + tab: 'string', + q: 'string', + }, + }, + }; + + var handlers = {}; + var currentTab = null; + var pendingDetailId = null; + var suppressHash = false; + + function parseQuery(qs) { + var out = {}; + if (!qs) return out; + qs.split('&').forEach(function (pair) { + if (!pair) return; + var idx = pair.indexOf('='); + var k = idx >= 0 ? pair.slice(0, idx) : pair; + var v = idx >= 0 ? pair.slice(idx + 1) : ''; + try { + k = decodeURIComponent(k.replace(/\+/g, ' ')); + v = decodeURIComponent(v.replace(/\+/g, ' ')); + } catch (e) { /* keep raw */ } + if (k) out[k] = v; + }); + return out; + } + + function parseHash() { + var raw = (location.hash || '#/dashboard').replace(/^#/, ''); + if (!raw.startsWith('/')) raw = '/' + raw; + + var qIdx = raw.indexOf('?'); + var pathStr = qIdx >= 0 ? raw.substring(0, qIdx) : raw; + var query = parseQuery(qIdx >= 0 ? raw.substring(qIdx + 1) : ''); + + var segments = pathStr.split('/').filter(function (s) { return s; }); + var head = segments[0] || 'dashboard'; + var tab = ROUTES['/' + head] || 'dashboard'; + var id = null; + if (segments[1] && DETAIL_TABS[tab]) { + var n = parseInt(segments[1], 10); + if (!isNaN(n) && n > 0) id = n; + } + + return { tab: tab, id: id, query: query }; + } + + function coerceField(type, val) { + if (type === 'int') { + var n = parseInt(val, 10); + return isNaN(n) ? null : n; + } + return val == null ? '' : String(val); + } + + function applyUrlToState(loc) { + loc = loc || parseHash(); + Object.keys(FILTER_DEFS).forEach(function (tab) { + if (loc.tab !== tab) return; + var def = FILTER_DEFS[tab]; + var st = State()[def.stateKey]; + if (!st) return; + Object.keys(def.defaults).forEach(function (key) { + st[key] = def.defaults[key]; + }); + Object.keys(def.fields).forEach(function (key) { + if (!(key in loc.query)) return; + var val = coerceField(def.fields[key], loc.query[key]); + if (val == null) return; + if (def.fields[key] === 'int' && val <= 0) return; + st[key] = val; + }); + if (tab === 'settings' && st.tab !== 'site' && st.tab !== 'all') { + st.tab = 'site'; + } + }); + } + + function shouldIncludeParam(key, val, defVal) { + if (val == null || val === '') return false; + if (typeof defVal === 'number') return val !== defVal; + return String(val) !== String(defVal); + } + + function buildHash(tab, opts) { + opts = opts || {}; + var path = TAB_TO_PATH[tab] || '/dashboard'; + if (opts.id && DETAIL_TABS[tab]) { + path += '/' + opts.id; + } + + var def = FILTER_DEFS[tab]; + var qs = ''; + if (def) { + var st = State()[def.stateKey] || {}; + var parts = []; + Object.keys(def.fields).forEach(function (key) { + var val = st[key]; + if (shouldIncludeParam(key, val, def.defaults[key])) { + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(val))); + } + }); + if (parts.length) qs = '?' + parts.join('&'); + } + + return '#' + path + qs; + } + + function setHash(hash) { + if (location.hash === hash) return; + suppressHash = true; + location.hash = hash; + suppressHash = false; + } + + function showTab(name, loc) { + var level = State().adminLevel || 'full'; + if (Perms() && !Perms().canAccessTab(name, level)) { + name = Perms().defaultTab(level); + var safeHash = buildHash(name); + if (location.hash !== safeHash) { + setHash(safeHash); + return; + } + } + if (!handlers[name]) name = 'dashboard'; + var prevTab = currentTab; + if (prevTab && prevTab !== name && window.AdminApi && typeof AdminApi.abortScope === 'function') { + AdminApi.abortScope(prevTab); + } + currentTab = name; + pendingDetailId = loc && loc.id ? loc.id : null; + + document.querySelectorAll('.panel').forEach(function (p) { + p.classList.remove('is-visible'); + }); + var panel = document.getElementById('panel-' + name); + if (panel) panel.classList.add('is-visible'); + document.querySelectorAll('.soon-admin-nav-item').forEach(function (li) { + li.classList.toggle('is-active', li.dataset.tab === name); + }); + if (handlers[name] && typeof handlers[name].render === 'function') { + handlers[name].render(); + } + } + + function onHashChange() { + if (suppressHash) return; + var loc = parseHash(); + applyUrlToState(loc); + showTab(loc.tab, loc); + } + + function navigate(tab) { + pendingDetailId = null; + var hash = buildHash(tab); + if (location.hash === hash) { + showTab(tab, parseHash()); + } else { + setHash(hash); + } + } + + function syncQuery(opts) { + if (!currentTab) return; + var hash = buildHash(currentTab, opts || {}); + setHash(hash); + } + + function openDetail(id) { + if (!currentTab || !DETAIL_TABS[currentTab]) return; + var n = parseInt(id, 10); + if (isNaN(n) || n <= 0) return; + setHash(buildHash(currentTab, { id: n })); + } + + function closeDetail() { + if (!currentTab || !DETAIL_TABS[currentTab]) return; + var loc = parseHash(); + if (!loc.id) return; + pendingDetailId = null; + setHash(buildHash(currentTab)); + } + + function hasPendingDetail() { + return pendingDetailId != null; + } + + function consumeDetailId() { + var id = pendingDetailId; + pendingDetailId = null; + return id; + } + + function onDrawerClosed() { + closeDetail(); + } + + function init(viewHandlers) { + handlers = viewHandlers || {}; + window.addEventListener('hashchange', onHashChange); + document.querySelectorAll('.soon-admin-nav-item').forEach(function (li) { + li.onclick = function () { + navigate(li.dataset.tab); + }; + }); + if (!location.hash) { + setHash('#/dashboard'); + } + var loc = parseHash(); + applyUrlToState(loc); + showTab(loc.tab, loc); + } + + window.AdminRouter = { + init: init, + navigate: navigate, + syncQuery: syncQuery, + openDetail: openDetail, + closeDetail: closeDetail, + hasPendingDetail: hasPendingDetail, + consumeDetailId: consumeDetailId, + onDrawerClosed: onDrawerClosed, + currentTab: function () { return currentTab; }, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/core/state.js b/frontend-web/pages/admin/assets/js/core/state.js new file mode 100644 index 0000000..54f15ec --- /dev/null +++ b/frontend-web/pages/admin/assets/js/core/state.js @@ -0,0 +1,14 @@ +(function () { + 'use strict'; + + // 分页与 URL 状态:docs/API-PAGINATION.md、AdminRouter + + window.AdminState = { + currentAdminId: null, + adminLevel: 'full', + users: { page: 1, size: 20, q: '' }, + orders: { page: 1, size: 20, q: '', status: '', refund_status: '', channel: '', from: '', to: '' }, + audits: { page: 1, size: 50, q: '', from: '', to: '', action: '' }, + settings: { tab: 'site', q: '' }, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/core/ui.js b/frontend-web/pages/admin/assets/js/core/ui.js new file mode 100644 index 0000000..87cdfe0 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/core/ui.js @@ -0,0 +1,246 @@ +(function () { + 'use strict'; + + var I = function () { return window.AdminI18n; }; + + function pageHeader(title, desc, actionsHtml) { + return '
    ' + + '
    ' + + '

    ' + I().esc(title) + '

    ' + + (desc ? '

    ' + I().esc(desc) + '

    ' : '') + + '
    ' + + (actionsHtml ? '
    ' + actionsHtml + '
    ' : '') + + '
    '; + } + + function dataTable(headers, rowsHtml, colSpan) { + var th = headers.map(function (h) { return '' + h + ''; }).join(''); + var empty = '' + + emptyBlock('暂无数据', '', '') + ''; + return '
    ' + th + + '' + (rowsHtml || empty) + '
    '; + } + + function emptyBlock(title, hint, actionHtml) { + var html = '
    ' + + '' + + '

    ' + I().esc(title) + '

    '; + if (hint) html += '

    ' + I().esc(hint) + '

    '; + if (actionHtml) html += actionHtml; + html += '
    '; + return html; + } + + function skeletonStats(n) { + var c = n || 4; + var html = '
    '; + for (var i = 0; i < c; i++) { + html += '
    '; + } + return html + '
    '; + } + + function skeletonTable() { + return '
    '; + } + + function pagerHtml(page, size, total) { + if (window.AdminPager) { + return AdminPager.html({ page: page, size: size, total: total }); + } + page = page || 1; + size = size || 20; + total = total || 0; + var pages = Math.max(1, Math.ceil(total / size)); + return '
    ' + + '' + + '第 ' + page + ' / ' + pages + ' 页(共 ' + total + ' 条)' + + '
    '; + } + + function bindPager(container, page, size, total, onPageChange) { + if (!container) return; + if (window.AdminPager) { + AdminPager.bind(container, { page: page, size: size, total: total }, function (chg) { + if (chg.size && chg.size !== size) { + onPageChange(1, chg.size); + } else if (chg.page) { + onPageChange(chg.page, size); + } + }); + return; + } + var prev = container.querySelector('.soon-pager-prev'); + var next = container.querySelector('.soon-pager-next'); + if (prev && !prev.disabled) { + prev.onclick = function () { onPageChange(Math.max(1, page - 1)); }; + } + if (next && !next.disabled) { + next.onclick = function () { + var pages = Math.max(1, Math.ceil(total / size)); + onPageChange(Math.min(pages, page + 1)); + }; + } + } + + function confirm(msg, onOk) { + if (typeof layer === 'undefined') { + if (window.confirm(msg)) onOk(); + return; + } + layer.confirm(msg, { skin: 'soon-layer', title: '确认' }, function (idx) { + layer.close(idx); + onOk(); + }); + } + + function openDrawer(title, bodyHtml) { + closeDrawer(); + var overlay = document.createElement('div'); + overlay.className = 'soon-admin-drawer-overlay'; + overlay.id = 'adminDrawerOverlay'; + overlay.onclick = closeDrawer; + + var drawer = document.createElement('div'); + drawer.className = 'soon-admin-drawer'; + drawer.id = 'adminDrawer'; + drawer.innerHTML = + '
    ' + + '

    ' + I().esc(title) + '

    ' + + '' + + '
    ' + bodyHtml + '
    '; + + drawer.querySelector('.soon-admin-drawer__close').onclick = closeDrawer; + document.body.appendChild(overlay); + document.body.appendChild(drawer); + } + + function closeDrawer() { + var o = document.getElementById('adminDrawerOverlay'); + var d = document.getElementById('adminDrawer'); + if (o) o.remove(); + if (d) d.remove(); + if (window.AdminRouter && typeof AdminRouter.onDrawerClosed === 'function') { + AdminRouter.onDrawerClosed(); + } + } + + function runFormSubmit(btn, handler, opts) { + opts = opts || {}; + if (!btn || btn.disabled) return; + var ret; + try { + ret = handler(); + } catch (e) { + return; + } + if (ret === false) return; + if (ret && typeof ret.then === 'function') { + setBtnLoading(btn, true, opts.loadingLabel); + ret.finally(function () { + setBtnLoading(btn, false); + }); + } + } + + function openFormModal(title, formHtml, onSave, opts) { + if (typeof layer === 'undefined') return; + opts = opts || {}; + layer.open({ + type: 1, + skin: 'soon-layer', + title: title, + area: opts.area || ['420px', 'auto'], + maxWidth: opts.maxWidth || '96vw', + content: formHtml, + success: function (layero) { + var area = opts.area || ['420px', 'auto']; + if (area[1] && area[1] !== 'auto') { + var contentEl = null; + if (layero && typeof layero.find === 'function') { + contentEl = layero.find('.layui-layer-content')[0]; + } else if (layero && layero.querySelector) { + contentEl = layero.querySelector('.layui-layer-content'); + } + if (contentEl) contentEl.classList.add('soon-layer-content--scroll'); + } + var btn = document.getElementById('adminModalSave'); + if (btn) { + btn.onclick = function () { + runFormSubmit(btn, onSave, opts); + }; + } + if (typeof opts.success === 'function') opts.success(); + }, + }); + } + + function bindFormSubmit(btnOrId, handler, loadingLabel) { + var btn = typeof btnOrId === 'string' ? document.getElementById(btnOrId) : btnOrId; + if (!btn) return; + btn.onclick = function () { + runFormSubmit(btn, handler, { loadingLabel: loadingLabel }); + }; + } + + function detailSection(title, rowsHtml) { + return '

    ' + I().esc(title) + '

    ' + rowsHtml + '
    '; + } + + function detailRow(label, value) { + return '
    ' + + I().esc(label) + '' + value + '
    '; + } + + function filterDateField(id, label, value) { + return '
    ' + + '' + I().esc(label) + '' + + '' + + '' + + '
    '; + } + + function listError(message, retryBtnId) { + return '
    ' + + '

    ' + I().esc(message || '加载失败') + '

    ' + + '
    '; + } + + function setBtnLoading(btn, loading, label) { + if (!btn) return; + if (loading) { + if (!btn.dataset.soonLabel) btn.dataset.soonLabel = btn.textContent; + btn.disabled = true; + btn.textContent = label || '处理中…'; + } else { + btn.disabled = false; + if (btn.dataset.soonLabel) btn.textContent = btn.dataset.soonLabel; + } + } + + function el(id) { return document.getElementById(id); } + + window.AdminUi = { + pageHeader: pageHeader, + dataTable: dataTable, + emptyBlock: emptyBlock, + skeletonStats: skeletonStats, + skeletonTable: skeletonTable, + pagerHtml: pagerHtml, + bindPager: bindPager, + confirm: confirm, + openDrawer: openDrawer, + closeDrawer: closeDrawer, + openFormModal: openFormModal, + runFormSubmit: runFormSubmit, + bindFormSubmit: bindFormSubmit, + detailSection: detailSection, + detailRow: detailRow, + filterDateField: filterDateField, + listError: listError, + setBtnLoading: setBtnLoading, + el: el, + }; +})(); diff --git a/frontend-web/pages/admin/assets/js/login.js b/frontend-web/pages/admin/assets/js/login.js new file mode 100644 index 0000000..48f540c --- /dev/null +++ b/frontend-web/pages/admin/assets/js/login.js @@ -0,0 +1,116 @@ +(function () { + 'use strict'; + + var TOKEN_KEY = 'soon_access'; + + function getV1Base() { + var cfg = window.SOON_DEPLOY_CONFIG || {}; + return (cfg.api_v1_base || '/api/v1').replace(/\/+$/, ''); + } + + function redirectTarget() { + var p = new URLSearchParams(location.search); + var r = p.get('redirect'); + if (r && typeof soonSafeRedirect === 'function') { + return soonSafeRedirect(r, 'index.html'); + } + return r || 'index.html'; + } + + function clearTokens() { + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem('soon_refresh'); + } catch (e) { /* ignore */ } + } + + function showError(msg) { + var err = document.getElementById('loginError'); + if (err) { + err.textContent = msg; + err.style.display = 'block'; + } + } + + async function verifyAdmin(token) { + var r = await fetch(getV1Base() + '/auth/me', { + headers: { Authorization: 'Bearer ' + token }, + }); + var j = await r.json(); + if (!j.ok || !j.data || j.data.role !== 'admin') return null; + return j.data; + } + + function init() { + if (localStorage.getItem(TOKEN_KEY)) { + verifyAdmin(localStorage.getItem(TOKEN_KEY)).then(function (me) { + if (me) location.href = redirectTarget(); + }); + } + + var pwdToggle = document.getElementById('pwdToggle'); + if (pwdToggle) { + pwdToggle.onclick = function () { + var p = document.getElementById('password'); + var show = p.type === 'password'; + p.type = show ? 'text' : 'password'; + this.textContent = show ? '隐藏' : '显示'; + }; + } + + var form = document.getElementById('loginForm'); + if (!form) return; + + form.onsubmit = function (e) { + e.preventDefault(); + var err = document.getElementById('loginError'); + if (err) { err.style.display = 'none'; err.textContent = ''; } + + var email = document.getElementById('email').value.trim(); + var password = document.getElementById('password').value; + + fetch(getV1Base() + '/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email, password: password }), + }).then(function (r) { return r.json(); }).then(function (j) { + if (!j.ok) { + showError(j.message || '登录失败'); + return; + } + localStorage.setItem(TOKEN_KEY, j.data.access_token); + localStorage.setItem('soon_refresh', j.data.refresh_token); + return verifyAdmin(j.data.access_token); + }).then(function (me) { + if (me === undefined) return; + if (!me) { + clearTokens(); + showError('无管理员权限,请使用管理员账号登录'); + soonToast('无管理员权限', 'warn'); + return; + } + location.href = redirectTarget(); + }).catch(function () { + showError('网络错误,请稍后重试'); + }); + }; + } + + function boot() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + } + + if (typeof layui !== 'undefined') { + layui.use(['layer'], function () { + window.layer = layui.layer; + layer.config({ skin: 'soon-layer' }); + boot(); + }); + } else { + boot(); + } +})(); diff --git a/frontend-web/pages/admin/assets/js/main.js b/frontend-web/pages/admin/assets/js/main.js new file mode 100644 index 0000000..db90be6 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/main.js @@ -0,0 +1,58 @@ +(function () { + 'use strict'; + + function init() { + if (!AdminApi.getToken()) { + AdminApi.goLogin(); + return; + } + + var headerMount = document.getElementById('adminHeaderMount'); + var env = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.env) || ''; + if (headerMount && window.AdminShell) { + AdminShell.mountHeader(headerMount, { env: env || '' }); + } + + AdminApi.fetchMe().then(function (me) { + if (!me || me.role !== 'admin') { + soonToast('需要管理员权限', 'warn'); + AdminApi.clearToken(); + setTimeout(AdminApi.goLogin, 600); + return; + } + AdminState.currentAdminId = me.id; + AdminState.adminLevel = AdminPermissions.normalizeLevel(me.admin_level); + var emailEl = document.getElementById('adminEmail'); + if (emailEl) { + var levelLabel = AdminState.adminLevel === 'ops' ? '运营' : ''; + emailEl.textContent = (me.email || '') + (levelLabel ? ' · ' + levelLabel : ''); + } + + document.getElementById('adminLogout').onclick = function () { + AdminApi.clearToken(); + AdminApi.goLogin(); + }; + + AdminPermissions.applyNav(AdminState.adminLevel); + AdminRouter.init(AdminViews); + }); + } + + function boot() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + } + + if (typeof layui !== 'undefined') { + layui.use(['layer'], function () { + window.layer = layui.layer; + layer.config({ skin: 'soon-layer' }); + boot(); + }); + } else { + boot(); + } +})(); diff --git a/frontend-web/pages/admin/assets/js/views/audits.js b/frontend-web/pages/admin/assets/js/views/audits.js new file mode 100644 index 0000000..f1cfc5e --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/audits.js @@ -0,0 +1,144 @@ +(function () { + 'use strict'; + + var Api = function () { return window.AdminApi; }; + var Ui = function () { return window.AdminUi; }; + var I = function () { return window.AdminI18n; }; + var State = function () { return window.AdminState; }; + var Filter = function () { return window.AdminFilter; }; + var Router = function () { return window.AdminRouter; }; + + var ACTION_OPTS = [ + { v: '', l: '全部操作' }, + { v: 'users.setStatus', l: '变更用户状态' }, + { v: 'users.create', l: '创建用户' }, + { v: 'users.update', l: '编辑用户' }, + { v: 'users.delete', l: '删除用户' }, + { v: 'users.setPassword', l: '重置密码' }, + { v: 'users.setSubscription', l: '设置会员' }, + { v: 'users.detail', l: '查看用户详情' }, + { v: 'orders.detail', l: '查看订单详情' }, + { v: 'orders.cancel', l: '取消订单' }, + { v: 'orders.refundApprove', l: '通过退款' }, + { v: 'orders.refundReject', l: '拒绝退款' }, + { v: 'plans.upsert', l: '编辑套餐' }, + { v: 'settings.set', l: '修改系统设置' }, + { v: 'settings.set_batch', l: '批量修改设置' }, + { v: 'payment.key.upload', l: '上传支付密钥' }, + ]; + + function buildQuery() { + var st = State().audits; + var q = 'audits?page=' + st.page + '&size=' + st.size; + if (st.q) q += '&q=' + encodeURIComponent(st.q); + if (st.from) q += '&from=' + encodeURIComponent(st.from); + if (st.to) q += '&to=' + encodeURIComponent(st.to); + if (st.action) q += '&action=' + encodeURIComponent(st.action); + return q; + } + + function actionOptions(selected) { + return ACTION_OPTS.map(function (o) { + return ''; + }).join(''); + } + + function formatContext(ctx) { + if (!ctx) return ''; + var body; + try { + body = '
    ' + I().esc(JSON.stringify(JSON.parse(ctx), null, 2)) + '
    '; + } catch (e) { + body = '
    ' + I().esc(ctx) + '
    '; + } + return '
    查看上下文' + body + '
    '; + } + + function renderFilterBar(st) { + return Filter().bar( + Filter().field('关键词', Filter().searchInput('auditQ', st.q, '操作、目标或管理员')) + + Filter().field('操作类型', Filter().select('auditAction', actionOptions(st.action))) + + Ui().filterDateField('auditFrom', '从', st.from) + + Ui().filterDateField('auditTo', '至', st.to), + '' + + '' + ); + } + + function bindFilters(st) { + var searchBtn = Ui().el('auditSearchBtn'); + var resetBtn = Ui().el('auditResetBtn'); + var qInput = Ui().el('auditQ'); + if (searchBtn) { + searchBtn.onclick = function () { + st.q = qInput ? qInput.value.trim() : ''; + st.action = Ui().el('auditAction').value; + st.from = Ui().el('auditFrom').value; + st.to = Ui().el('auditTo').value; + if (st.from && st.to && st.from > st.to) { + soonToast('开始日期不能晚于结束日期', 'warn'); + return; + } + st.page = 1; + Router().syncQuery(); + render(); + }; + } + if (qInput) { + qInput.onkeydown = function (e) { + if (e.key === 'Enter' && searchBtn) searchBtn.click(); + }; + } + if (resetBtn) { + resetBtn.onclick = function () { + State().audits = { page: 1, size: 50, q: '', from: '', to: '', action: '' }; + Router().syncQuery(); + render(); + }; + } + } + + function renderTimeline(items) { + if (!items.length) { + return Ui().emptyBlock('暂无审计记录', '调整筛选条件或等待管理员操作产生日志', ''); + } + return items.map(function (a) { + return '
    ' + + I().esc(I().tAction(a.action)) + ' · ' + I().esc(a.target) + '
    ' + + '
    ' + I().esc(a.admin_email || '') + ' · ' + + I().formatDateTime(a.created_at) + '
    ' + formatContext(a.context) + '
    '; + }).join(''); + } + + function render() { + var panel = Ui().el('panel-audits'); + if (!panel) return; + var st = State().audits; + var header = Ui().pageHeader('审计日志', '管理员操作记录,支持按操作类型与时间筛选'); + panel.innerHTML = '
    ' + header + renderFilterBar(st) + Ui().skeletonTable() + '
    '; + + Api().get(buildQuery()).then(function (j) { + if (!j) { + panel.innerHTML = '
    ' + header + renderFilterBar(st) + + Ui().listError('审计日志加载失败', 'auditsListRetry') + '
    '; + bindFilters(st); + Ui().el('auditsListRetry').onclick = render; + return; + } + var items = j.data.items || []; + panel.innerHTML = '
    ' + header + renderFilterBar(st) + + '
    ' + renderTimeline(items) + '
    ' + + Ui().pagerHtml(j.data.page, j.data.size, j.data.total) + '
    '; + bindFilters(st); + Ui().bindPager(panel, j.data.page, j.data.size, j.data.total, function (p, newSize) { + st.page = p; + if (newSize) st.size = newSize; + Router().syncQuery(); + render(); + }); + }); + } + + window.AdminViews = window.AdminViews || {}; + window.AdminViews.audits = { render: render }; +})(); diff --git a/frontend-web/pages/admin/assets/js/views/dashboard.js b/frontend-web/pages/admin/assets/js/views/dashboard.js new file mode 100644 index 0000000..a42a7a9 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/dashboard.js @@ -0,0 +1,122 @@ +(function () { + 'use strict'; + + var Api = function () { return window.AdminApi; }; + var Ui = function () { return window.AdminUi; }; + var I = function () { return window.AdminI18n; }; + + function statCard(label, value, opts) { + opts = opts || {}; + var inner = '
    ' + I().esc(label) + + '
    ' + I().esc(value) + '
    '; + if (opts.hint) inner += '
    ' + I().esc(opts.hint) + '
    '; + if (opts.href) { + return '' + inner + ''; + } + return '
    ' + inner + '
    '; + } + + function alertBar(d) { + var parts = []; + if (d.orders_pending > 0) parts.push('待支付订单 ' + d.orders_pending + ' 笔'); + if (d.refunds_pending > 0) parts.push('退款待审核 ' + d.refunds_pending + ' 笔'); + if (!parts.length) return ''; + var href = '#/orders'; + if (d.refunds_pending > 0) href = '#/orders?refund_status=pending'; + else if (d.orders_pending > 0) href = '#/orders?status=pending'; + return '
    ' + parts.join(' · ') + '' + + '去处理
    '; + } + + function quickLinks() { + var links = [ + { href: '#/users', label: '用户管理' }, + { href: '#/orders', label: '订单管理' }, + { href: '#/plans', label: '套餐配置' }, + { href: '#/payment', label: '支付密钥' }, + { href: '#/settings', label: '系统设置' }, + { href: '#/audits', label: '审计日志' }, + ]; + var level = (window.AdminState && AdminState.adminLevel) || 'full'; + var visible = window.AdminPermissions + ? AdminPermissions.filterQuickLinks(links, level) + : links; + var items = visible.map(function (l) { + return '' + I().esc(l.label) + ''; + }).join(''); + return '

    快捷入口

    ' + + '
    ' + items + '
    '; + } + + function recentOrdersTable(orders) { + var rows = (orders || []).map(function (o) { + return '' + I().esc(o.order_no || o.id) + '' + I().esc(o.email || '') + '' + + I().formatCents(o.amount_cents) + '' + I().badgeStatus(o.status) + '' + + I().formatDateTime(o.created_at) + ''; + }).join(''); + return Ui().dataTable(['订单号', '用户', '金额', '状态', '时间'], rows, 5); + } + + function render() { + var panel = Ui().el('panel-dashboard'); + if (!panel) return; + panel.innerHTML = '
    ' + + Ui().pageHeader('运营概览', '关键指标、待办提醒与最近订单', + '') + + Ui().skeletonStats(6) + + '
    ' + + '

    最近订单

    ' + Ui().skeletonTable() + '
    ' + + '
    '; + + function fetchStats(showLoading) { + var btn = Ui().el('dashRefresh'); + if (showLoading && btn) Ui().setBtnLoading(btn, true, '刷新中…'); + Api().get('stats').then(function (j) { + if (showLoading && btn) Ui().setBtnLoading(btn, false); + if (!j) { + panel.innerHTML = '
    ' + + Ui().pageHeader('运营概览', '关键指标、待办提醒与最近订单', + '') + + Ui().listError('概览数据加载失败', 'dashRetry') + '
    '; + Ui().el('dashRetry').onclick = render; + Ui().el('dashRefresh').onclick = function () { fetchStats(true); }; + return; + } + paintDashboard(panel, j.data); + }); + } + + Ui().el('dashRefresh').onclick = function () { fetchStats(true); }; + fetchStats(false); + } + + function paintDashboard(panel, d) { + var stats = + statCard('用户总数', d.users, { href: '#/users', hint: '点击进入用户管理' }) + + statCard('订单总数', d.orders, { href: '#/orders', hint: '含全部状态' }) + + statCard('今日订单', d.orders_today, { href: '#/orders', hint: '按创建日统计' }) + + statCard('待支付', d.orders_pending || 0, { href: '#/orders?status=pending', hint: '需关注超时未付' }) + + statCard('活跃订阅', d.active_subscriptions, { href: '#/users', hint: '有效会员数' }) + + statCard('今日收入', I().formatCents(d.revenue_today_cents), { href: '#/orders', hint: '已支付订单' }) + + statCard('云端文件', d.files_total, { hint: '未删除的设计文件' }) + + statCard('退款待审', d.refunds_pending || 0, { href: '#/orders?refund_status=pending', hint: '需在订单页处理' }); + + panel.innerHTML = '
    ' + + Ui().pageHeader('运营概览', '关键指标、待办提醒与最近订单', + '' + + '全部订单') + + alertBar(d) + + '
    ' + stats + '
    ' + + '
    ' + + '
    ' + + '

    最近订单

    ' + + '查看全部
    ' + + recentOrdersTable(d.recent_orders) + '
    ' + + quickLinks() + '
    '; + + Ui().el('dashRefresh').onclick = render; + } + + window.AdminViews = window.AdminViews || {}; + window.AdminViews.dashboard = { render: render }; +})(); diff --git a/frontend-web/pages/admin/assets/js/views/orders.js b/frontend-web/pages/admin/assets/js/views/orders.js new file mode 100644 index 0000000..0979150 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/orders.js @@ -0,0 +1,334 @@ +(function () { + 'use strict'; + + var Api = function () { return window.AdminApi; }; + var Ui = function () { return window.AdminUi; }; + var I = function () { return window.AdminI18n; }; + var State = function () { return window.AdminState; }; + var Filter = function () { return window.AdminFilter; }; + var Menu = function () { return window.AdminMenu; }; + var Router = function () { return window.AdminRouter; }; + + function buildQuery() { + var st = State().orders; + var q = 'orders?page=' + st.page + '&size=' + st.size; + if (st.q) q += '&q=' + encodeURIComponent(st.q); + if (st.status) q += '&status=' + encodeURIComponent(st.status); + if (st.refund_status) q += '&refund_status=' + encodeURIComponent(st.refund_status); + if (st.channel) q += '&channel=' + encodeURIComponent(st.channel); + if (st.from) q += '&from=' + encodeURIComponent(st.from); + if (st.to) q += '&to=' + encodeURIComponent(st.to); + return q; + } + + function statusOptions(selected) { + var opts = [{ v: '', l: '全部状态' }, { v: 'pending', l: '待支付' }, { v: 'paid', l: '已支付' }, + { v: 'cancelled', l: '已取消' }, { v: 'refunded', l: '已退款' }]; + return opts.map(function (o) { + return ''; + }).join(''); + } + + function refundStatusOptions(selected) { + var opts = [{ v: '', l: '全部退款' }, { v: 'pending', l: '待审核' }, { v: 'none', l: '无申请' }, + { v: 'approved', l: '已通过' }, { v: 'rejected', l: '已拒绝' }]; + return opts.map(function (o) { + return ''; + }).join(''); + } + + function channelOptions(selected) { + var opts = [{ v: '', l: '全部渠道' }, { v: 'alipay', l: '支付宝' }, { v: 'wechat', l: '微信支付' }]; + return opts.map(function (o) { + return ''; + }).join(''); + } + + function renderFilterBar(st) { + return Filter().bar( + Filter().field('关键词', Filter().searchInput('orderQ', st.q, '订单号或邮箱')) + + Filter().field('状态', Filter().select('orderStatus', statusOptions(st.status))) + + Filter().field('退款', Filter().select('orderRefundStatus', refundStatusOptions(st.refund_status))) + + Filter().field('渠道', Filter().select('orderChannel', channelOptions(st.channel))) + + Ui().filterDateField('orderFrom', '从', st.from) + + Ui().filterDateField('orderTo', '至', st.to), + '' + + '' + ); + } + + function orderActionMenu(o) { + var items = [{ label: '详情', action: 'detail', dataset: { id: String(o.id) } }]; + if (o.status === 'pending') { + items.push({ label: '取消', action: 'cancel', dataset: { id: String(o.id) } }); + } + if (o.status === 'paid' && o.refund_status === 'pending') { + items.push({ label: '通过退款', action: 'refund_approve', dataset: { id: String(o.id) } }); + items.push({ label: '拒绝退款', action: 'refund_reject', dataset: { id: String(o.id) } }); + } + if (o.status === 'paid' && (o.refund_status === 'none' || !o.refund_status)) { + items.push({ label: '退款', action: 'refund_direct', dataset: { id: String(o.id) } }); + } + return Menu().actionMenu('order-' + o.id, items); + } + + function bindOrderMenus(root) { + Menu().bind(root, function (action, btn) { + var id = parseInt(btn.dataset.id, 10); + if (action === 'detail') showOrderDetail(id); + if (action === 'cancel') { + Ui().confirm('确认取消该待支付订单?', function () { + Api().postJson('orders/' + id + '/cancel', {}).then(function (r) { + if (r) { soonToast('订单已取消', 'success'); Ui().closeDrawer(); render(); } + }); + }); + } + if (action === 'refund_approve' || action === 'refund_direct') { + var title = action === 'refund_direct' ? '管理员退款' : '通过退款申请'; + openNoteModal(title, '可选:填写审核备注', function (note) { + return Api().postJson('orders/' + id + '/refund/approve', { note: note }).then(function (r) { + if (r) { soonToast('退款已处理', 'success'); Ui().closeDrawer(); render(); } + }); + }); + } + if (action === 'refund_reject') { + openNoteModal('拒绝退款', '请填写拒绝原因', function (note) { + if (!note) { + soonToast('请填写拒绝原因', 'warn'); + return false; + } + return Api().postJson('orders/' + id + '/refund/reject', { note: note }).then(function (r) { + if (r) { soonToast('已拒绝退款', 'success'); Ui().closeDrawer(); render(); } + }); + }); + } + }); + } + + function applyFilters() { + var st = State().orders; + st.q = Ui().el('orderQ').value.trim(); + st.status = Ui().el('orderStatus').value; + st.refund_status = Ui().el('orderRefundStatus').value; + st.channel = Ui().el('orderChannel').value; + st.from = Ui().el('orderFrom').value; + st.to = Ui().el('orderTo').value; + if (st.from && st.to && st.from > st.to) { + soonToast('开始日期不能晚于结束日期', 'warn'); + return false; + } + st.page = 1; + return true; + } + + function bindFilters() { + Ui().el('orderSearchBtn').onclick = function () { + if (applyFilters()) { + Router().syncQuery(); + render(); + } + }; + Ui().el('orderResetBtn').onclick = function () { + State().orders = { + page: 1, size: 20, q: '', status: '', refund_status: '', channel: '', from: '', to: '', + }; + Router().syncQuery(); + render(); + }; + } + + function rowActions(o) { + var btns = ''; + if (o.status === 'pending') { + btns += ''; + } + if (o.status === 'paid' && o.refund_status === 'pending') { + btns += ''; + btns += ''; + } + if (o.status === 'paid' && (o.refund_status === 'none' || !o.refund_status)) { + btns += ''; + } + return '
    ' + btns + '
    '; + } + + function openNoteModal(title, placeholder, onSave) { + var form = '
    ' + + '
    ' + + '
    ' + + '
    '; + Ui().openFormModal(title, form, function () { + return onSave(Ui().el('orderNote').value.trim()); + }); + } + + function showOrderDetail(id, opts) { + opts = opts || {}; + if (!opts.fromRoute) Router().openDetail(id); + Api().get('orders/' + id).then(function (j) { + if (!j) return; + var o = j.data.order || {}; + var sub = j.data.subscription; + + var basic = Ui().detailRow('订单号', I().esc(o.order_no)) + + Ui().detailRow('用户', I().esc(o.email || '')) + + Ui().detailRow('套餐', I().esc(o.plan_name || '-')) + + Ui().detailRow('渠道', I().esc(I().tChannel(o.channel))) + + Ui().detailRow('金额', I().formatCents(o.amount_cents)) + + Ui().detailRow('订单状态', I().badgeStatus(o.status)) + + Ui().detailRow('退款状态', I().badgeRefundStatus(o.refund_status)) + + Ui().detailRow('交易号', I().esc(o.txn_id || '-')) + + Ui().detailRow('创建时间', I().esc(I().formatDateTime(o.created_at))) + + Ui().detailRow('支付时间', I().esc(I().formatDateTime(o.paid_at))) + + Ui().detailRow('取消时间', I().esc(I().formatDateTime(o.cancelled_at))) + + Ui().detailRow('退款时间', I().esc(I().formatDateTime(o.refunded_at))); + + var refundHtml = ''; + if (o.refund_reason) { + refundHtml += Ui().detailRow('退款原因', I().esc(o.refund_reason)); + } + if (o.refund_note) { + refundHtml += Ui().detailRow('审核备注', I().esc(o.refund_note)); + } + if (!refundHtml) { + refundHtml = '

    无退款记录

    '; + } + + var subHtml; + if (sub) { + subHtml = Ui().detailRow('订阅状态', I().badgeStatus(sub.status)) + + Ui().detailRow('开始', I().esc(I().formatDateTime(sub.started_at))) + + Ui().detailRow('到期', I().esc(I().formatDateTime(sub.expires_at))); + } else { + subHtml = '

    无关联订阅

    '; + } + + var drawerActions = rowActions(o).replace(/]*order-detail-btn[^>]*>[\s\S]*?<\/button>/, ''); + Ui().openDrawer('订单 ' + I().esc(o.order_no), + '
    ' + drawerActions + '
    ' + + Ui().detailSection('订单信息', basic) + + Ui().detailSection('退款信息', refundHtml) + + Ui().detailSection('关联会员', subHtml)); + + var drawer = document.getElementById('adminDrawer'); + if (drawer) bindRowActions(drawer); + }); + } + + function bindRowActions(root) { + if (!root) return; + root.querySelectorAll('.order-detail-btn').forEach(function (btn) { + btn.onclick = function () { showOrderDetail(parseInt(btn.dataset.id, 10)); }; + }); + root.querySelectorAll('.order-cancel-btn').forEach(function (btn) { + btn.onclick = function () { + Ui().confirm('确认取消该待支付订单?', function () { + Api().postJson('orders/' + btn.dataset.id + '/cancel', {}).then(function (r) { + if (r) { + soonToast('订单已取消', 'success'); + Ui().closeDrawer(); + render(); + } + }); + }); + }; + }); + root.querySelectorAll('.order-refund-approve-btn, .order-refund-direct-btn').forEach(function (btn) { + btn.onclick = function () { + var isDirect = btn.classList.contains('order-refund-direct-btn'); + var title = isDirect ? '管理员退款' : '通过退款申请'; + openNoteModal(title, '可选:填写审核备注', function (note) { + return Api().postJson('orders/' + btn.dataset.id + '/refund/approve', { note: note }).then(function (r) { + if (r) { + layer.closeAll(); + var gw = r.data && r.data.gateway; + var msg = '退款已完成'; + if (gw && gw.indexOf('skipped') === 0) msg += '(未配置支付密钥,仅标记退款并撤销会员)'; + else if (gw && gw.indexOf('fail') >= 0) msg += '(支付渠道退款失败,已标记退款并撤销会员)'; + soonToast(msg, 'success'); + Ui().closeDrawer(); + render(); + } + }); + }); + }; + }); + root.querySelectorAll('.order-refund-reject-btn').forEach(function (btn) { + btn.onclick = function () { + openNoteModal('拒绝退款', '请填写拒绝原因', function (note) { + if (!note) { + soonToast('请填写拒绝原因', 'warn'); + return false; + } + return Api().postJson('orders/' + btn.dataset.id + '/refund/reject', { note: note }).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('已拒绝退款申请', 'success'); + Ui().closeDrawer(); + render(); + } + }); + }); + }; + }); + } + + function render() { + var panel = Ui().el('panel-orders'); + if (!panel) return; + if (!Router().hasPendingDetail()) Ui().closeDrawer(); + var st = State().orders; + panel.innerHTML = '
    ' + + Ui().pageHeader('订单管理', '支付订单查询、取消与退款审核') + + renderFilterBar(st) + Ui().skeletonTable() + '
    '; + + Api().get(buildQuery()).then(function (j) { + if (!j) { + panel.innerHTML = '
    ' + + Ui().pageHeader('订单管理', '支付订单查询、取消与退款审核') + + renderFilterBar(st) + Ui().listError('订单列表加载失败', 'ordersListRetry') + '
    '; + bindFilters(); + Ui().el('ordersListRetry').onclick = render; + var errDetailId = Router().consumeDetailId(); + if (errDetailId) showOrderDetail(errDetailId, { fromRoute: true }); + return; + } + var rows = (j.data.items || []).map(function (o) { + return '' + I().esc(o.order_no || o.id) + '' + I().esc(o.email || '') + '' + + I().esc(o.plan_name || '-') + '' + I().esc(I().tChannel(o.channel)) + '' + + I().formatCents(o.amount_cents) + '' + I().badgeStatus(o.status) + '' + + I().badgeRefundStatus(o.refund_status) + '' + I().formatDateTime(o.created_at) + '' + + I().formatDateTime(o.paid_at) + '' + orderActionMenu(o) + ''; + }).join(''); + + panel.innerHTML = '
    ' + + Ui().pageHeader('订单管理', '支付订单查询、取消与退款审核') + + renderFilterBar(st) + + Ui().dataTable( + ['订单号', '用户', '套餐', '渠道', '金额', '状态', '退款', '创建时间', '支付时间', '操作'], + rows + ) + + Ui().pagerHtml(j.data.page, j.data.size, j.data.total) + '
    '; + + bindFilters(); + Ui().bindPager(panel, j.data.page, j.data.size, j.data.total, function (p, newSize) { + st.page = p; + if (newSize) st.size = newSize; + Router().syncQuery(); + render(); + }); + bindOrderMenus(panel); + var detailId = Router().consumeDetailId(); + if (detailId) showOrderDetail(detailId, { fromRoute: true }); + }); + } + + window.AdminViews = window.AdminViews || {}; + window.AdminViews.orders = { render: render }; +})(); diff --git a/frontend-web/pages/admin/assets/js/views/payment.js b/frontend-web/pages/admin/assets/js/views/payment.js new file mode 100644 index 0000000..466d81c --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/payment.js @@ -0,0 +1,290 @@ +(function () { + + 'use strict'; + + + + var Api = function () { return window.AdminApi; }; + + var Ui = function () { return window.AdminUi; }; + + var I = function () { return window.AdminI18n; }; + + var Form = function () { return window.AdminForm; }; + + var Tabs = function () { return window.AdminTabs; }; + + + + var CHANNEL_CONFIG = { + + alipay: { + + label: '支付宝', + + kinds: [ + + { kind: 'private_key', file: 'alipay_private_key.pem', label: '应用私钥 private_key' }, + + { kind: 'public_key', file: 'alipay_public_key.pem', label: '支付宝公钥 public_key' }, + + ], + + }, + + wechat: { + + label: '微信支付', + + kinds: [ + + { kind: 'mch_private_key', file: 'wechat_mch_private_key.pem', label: '商户私钥 mch_private_key' }, + + { kind: 'api_v3_key', file: 'wechat_api_v3_key.pem', label: 'APIv3 密钥 api_v3_key' }, + + { kind: 'platform_cert', file: 'wechat_platform_cert.pem', label: '平台证书 platform_cert' }, + + ], + + }, + + }; + + + + function fileSet(items) { + + var set = {}; + + (items || []).forEach(function (p) { set[p.name] = p; }); + + return set; + + } + + + + function channelFormHtml(channel, files) { + + var cfg = CHANNEL_CONFIG[channel]; + + var rows = cfg.kinds.map(function (k) { + + var ok = !!files[k.file]; + + var statusCls = ok ? 'soon-admin-key-form-row__status--ok' : 'soon-admin-key-form-row__status--miss'; + + var statusText = ok ? '已就绪' : '缺失'; + + var id = channel + '_' + k.kind; + + return '
    ' + + + '
    ' + I().esc(k.label) + '
    ' + + + '
    ' + statusText + '
    ' + + + '' + + + '
    '; + + }).join(''); + + return '
    ' + Form().panel(cfg.label + ' 密钥', rows) + + + '
    回调说明' + + + '

    notify:/api/v1/pay/' + channel + '/notify(须配置 site.base_url 为 HTTPS 外网地址)

    ' + + + '
    拖拽 .pem 到此处自动填入对应字段
    '; + + } + + + + function filesTabHtml(items) { + + var rows = (items || []).map(function (p) { + + return '' + I().esc(p.name) + '' + p.bytes + ' B' + + + I().formatDateTime(p.mtime) + ''; + + }).join(''); + + return Ui().dataTable(['文件名', '大小', '修改时间'], rows); + + } + + + + function uploadPem(channel, kind, content, onDone) { + + return Api().fetch('payment/upload', { + + method: 'POST', + + body: JSON.stringify({ channel: channel, kind: kind, content: content }), + + }).then(function (j) { + + if (j) { + + soonToast('密钥已上传', 'success'); + + if (onDone) onDone(); + + } + + return j; + + }); + + } + + + + function bindChannel(channel, panel) { + + panel.querySelectorAll('.pay-upload-btn').forEach(function (btn) { + + btn.onclick = function () { + + var target = Ui().el(btn.dataset.target); + + var content = target ? target.value.trim() : ''; + + if (!content) { + + soonToast('请先粘贴或拖入 PEM 内容', 'warn'); + + return; + + } + + Ui().runFormSubmit(btn, function () { + return uploadPem(btn.dataset.channel, btn.dataset.kind, content, render); + }); + + }; + + }); + + + + var drop = Ui().el('payDrop_' + channel); + + if (!drop) return; + + drop.ondragover = function (e) { e.preventDefault(); drop.classList.add('is-dragover'); }; + + drop.ondragleave = function () { drop.classList.remove('is-dragover'); }; + + drop.ondrop = function (e) { + + e.preventDefault(); + + drop.classList.remove('is-dragover'); + + var file = e.dataTransfer.files[0]; + + if (!file) return; + + file.text().then(function (text) { + + var name = file.name.toLowerCase(); + + var cfg = CHANNEL_CONFIG[channel]; + + var matched = cfg.kinds.find(function (k) { return name.indexOf(k.kind.replace(/_/g, '')) >= 0 || name === k.file; }); + + var targetId = matched ? channel + '_' + matched.kind : channel + '_' + cfg.kinds[0].kind; + + var ta = Ui().el(targetId); + + if (ta) ta.value = text; + + soonToast('已填入 ' + (matched ? matched.label : '首个字段') + ',请确认后点击上传', 'success'); + + }); + + }; + + } + + + + function render() { + + var panel = Ui().el('panel-payment'); + + if (!panel) return; + + panel.innerHTML = '
    ' + Ui().pageHeader('支付配置', '按渠道管理密钥文件,上传后立即热加载') + + + Ui().skeletonTable() + '
    '; + + + + Api().get('payment/status').then(function (j) { + + if (!j) return; + + var items = j.data.items || []; + + var files = fileSet(items); + + + + var tabsHtml = Tabs().html('paymentTabs', [ + + { id: 'alipay', label: '支付宝' }, + + { id: 'wechat', label: '微信支付' }, + + { id: 'files', label: '文件清单' }, + + ], { + + alipay: channelFormHtml('alipay', files), + + wechat: channelFormHtml('wechat', files), + + files: filesTabHtml(items), + + }, 'alipay'); + + + + panel.innerHTML = '
    ' + + + Ui().pageHeader('支付配置', '按渠道管理密钥文件,上传后立即热加载') + + + tabsHtml + '
    '; + + + + var tabsRoot = Ui().el('paymentTabs'); + + Tabs().bind(tabsRoot); + + bindChannel('alipay', panel); + + bindChannel('wechat', panel); + + }); + + } + + + + window.AdminViews = window.AdminViews || {}; + + window.AdminViews.payment = { render: render }; + +})(); + diff --git a/frontend-web/pages/admin/assets/js/views/plans.js b/frontend-web/pages/admin/assets/js/views/plans.js new file mode 100644 index 0000000..3c0beb8 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/plans.js @@ -0,0 +1,253 @@ +(function () { + 'use strict'; + + var Api = function () { return window.AdminApi; }; + var Ui = function () { return window.AdminUi; }; + var I = function () { return window.AdminI18n; }; + var Form = function () { return window.AdminForm; }; + + function activeBadge(v) { + var on = v === true || parseInt(v, 10) === 1; + return I().badgeStatus(on ? 'active' : 'disabled'); + } + + function recBadge(v) { + if (!v) return '—'; + return '推荐'; + } + + function isFreePlan(plan) { + return plan && plan.code === 'free'; + } + + function isPaidMemberPlan(plan) { + return plan && plan.code !== 'free' && plan.price_cents > 0; + } + + function editableFeatures(plan) { + if (plan.features_editable && plan.features_editable.length) { + return plan.features_editable.slice(); + } + var lines = plan.features || []; + return lines.filter(function (l) { + return l.indexOf('订阅有效期') < 0 + && l.indexOf('订阅周期') < 0 + && l.indexOf('到期') < 0; + }); + } + + function featureRowsHtml(lines) { + var items = lines && lines.length ? lines : ['']; + return items.map(function (text) { + return '
    ' + + '' + + '
    '; + }).join(''); + } + + function bindFeatureEditor() { + var list = document.getElementById('peFeatures'); + var addBtn = document.getElementById('peFeatureAdd'); + if (!list) return; + + function bindDel(btn) { + if (!btn || btn.dataset.bound) return; + btn.dataset.bound = '1'; + btn.onclick = function () { + var rows = list.querySelectorAll('.soon-admin-feature-row'); + if (rows.length <= 1) { + var inp = list.querySelector('.soon-admin-feature-input'); + if (inp) inp.value = ''; + return; + } + var row = btn.closest('.soon-admin-feature-row'); + if (row) row.remove(); + }; + } + + list.querySelectorAll('.soon-admin-feature-del').forEach(bindDel); + + if (addBtn && !addBtn.dataset.bound) { + addBtn.dataset.bound = '1'; + addBtn.onclick = function () { + var row = document.createElement('div'); + row.className = 'soon-admin-feature-row'; + row.innerHTML = + '' + + ''; + list.appendChild(row); + bindDel(row.querySelector('.soon-admin-feature-del')); + row.querySelector('.soon-admin-feature-input').focus(); + }; + } + } + + function collectFeatures() { + var lines = []; + document.querySelectorAll('#peFeatures .soon-admin-feature-input').forEach(function (inp) { + var t = inp.value.trim(); + if (t) lines.push(t); + }); + return lines; + } + + function periodHint(plan) { + if (isFreePlan(plan)) return ''; + var days = parseInt(Ui().el('peDays') && Ui().el('peDays').value, 10); + if (!days || days <= 0) days = plan.duration_days || 0; + if (days <= 0) return '付费方案保存后将自动追加订阅周期说明。'; + var label = days >= 365 ? '1 年' : (days === 90 ? '1 季' : (days >= 30 && days % 30 === 0 ? (days / 30) + ' 个月' : days + ' 天')); + return '保存后将自动追加订阅周期说明(' + label + '),用户端卡片中不展示该行'; + } + + function openEditModal(plan) { + var free = isFreePlan(plan); + var paid = isPaidMemberPlan(plan); + var features = editableFeatures(plan); + + var basicPanel = Form().panel('基本信息', + Form().field('代码', '') + + Form().field('名称', Form().input('peName', { value: plan.name })) + + Form().field('简介', Form().input('peDesc', { value: plan.description || '' })) + ); + + var pricePanel = Form().panel('价格与周期', + '
    ' + + Form().field('价格(元)', + Form().input('pePriceYuan', { + type: 'number', + value: (plan.price_cents / 100).toFixed(2), + disabled: free, + })) + + Form().field('有效天数', + Form().input('peDays', { type: 'number', value: plan.duration_days }), + free ? '免费版填 0' : '月付 30 / 季付 90 / 年付 365') + + Form().field('排序', + Form().input('peSort', { type: 'number', value: plan.sort_order })) + + '
    ' + + '
    ' + + '' + + '' + + (paid ? '' : '') + + '
    ' + ); + + var quotaPanel = Form().panel('云端配额', + '
    ' + + Form().field('存储(MB)', Form().input('peQuota', { type: 'number', value: plan.quota_mb })) + + Form().field('文件数上限', Form().input('peMaxFiles', { type: 'number', value: plan.max_files })) + + '
    ', + '云端存储与文件数量上限;预览/保存/导出仍受订阅状态约束。' + ); + + var benefitTitle = free ? '免费版展示' : '订阅包含'; + var benefitHint = free + ? '展示在会员中心免费版区域;补充说明写在简介中。' + : periodHint(plan); + var benefitPanel = Form().panel(benefitTitle, + '
    ' + featureRowsHtml(features) + '
    ' + + '' + + (benefitHint ? '

    ' + I().esc(benefitHint) + '

    ' : '') + ); + + var form = '
    ' + + basicPanel + pricePanel + quotaPanel + benefitPanel + + Form().actions('保存', 'adminModalSave') + + '
    '; + + Ui().openFormModal((free ? '免费版' : '订阅方案') + ' · ' + plan.name, form, function () { + var name = Ui().el('peName').value.trim(); + if (!name) { + soonToast('名称不能为空', 'warn'); + return false; + } + var featureLines = collectFeatures(); + if (!featureLines.length) { + soonToast('请至少填写一条权益', 'warn'); + return false; + } + var yuan = free ? 0 : (parseFloat(Ui().el('pePriceYuan').value) || 0); + var body = { + code: plan.code, + name: name, + description: Ui().el('peDesc').value.trim(), + price_cents: Math.round(yuan * 100), + quota_mb: parseInt(Ui().el('peQuota').value, 10) || 0, + max_files: parseInt(Ui().el('peMaxFiles').value, 10) || 0, + duration_days: parseInt(Ui().el('peDays').value, 10) || 0, + sort_order: parseInt(Ui().el('peSort').value, 10) || 0, + is_recommended: free ? 0 : (Ui().el('peRec').checked ? 1 : 0), + is_active: Ui().el('peActive').checked ? 1 : 0, + features: featureLines, + sync_member_benefits: paid && Ui().el('peSync') && Ui().el('peSync').checked ? 1 : 0, + }; + return Api().postJson('plans', body).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('已保存', 'success'); + render(); + } + }); + }, { + area: ['640px', '72vh'], + success: function () { + bindFeatureEditor(); + var daysEl = Ui().el('peDays'); + var hintEl = document.querySelector('.soon-admin-form-hint--tight'); + if (daysEl && hintEl && paid) { + daysEl.oninput = function () { + hintEl.textContent = periodHint(plan); + }; + } + }, + }); + } + + function featureSummary(plan) { + var n = editableFeatures(plan).length; + return n > 0 ? n + ' 条' : '—'; + } + + function render() { + var panel = Ui().el('panel-plans'); + if (!panel) return; + panel.innerHTML = '
    ' + + Ui().pageHeader('订阅方案', '配置价格、周期、配额与用户端展示文案') + + Ui().skeletonTable() + '
    '; + + Api().get('plans').then(function (j) { + if (!j) return; + var items = j.data.items || []; + var rows = items.map(function (p) { + return '' + I().esc(p.code) + '' + I().esc(p.name) + + '' + I().formatCents(p.price_cents) + '' + featureSummary(p) + '' + + p.quota_mb + ' MB' + (p.duration_days > 0 ? p.duration_days + ' 天' : '永久') + '' + + recBadge(p.is_recommended) + '' + activeBadge(p.is_active) + + ''; + }).join(''); + + panel.innerHTML = '
    ' + + Ui().pageHeader('订阅方案', '配置价格、周期、配额与用户端展示文案') + + Ui().dataTable( + ['代码', '名称', '价格', '权益', '存储', '有效期', '推荐', '状态', '操作'], + rows + ) + '
    '; + + var planMap = {}; + items.forEach(function (p) { planMap[p.code] = p; }); + + panel.querySelectorAll('.plan-edit').forEach(function (btn) { + btn.onclick = function () { + var code = btn.closest('tr').dataset.code; + if (planMap[code]) openEditModal(planMap[code]); + }; + }); + }); + } + + window.AdminViews = window.AdminViews || {}; + window.AdminViews.plans = { render: render }; +})(); diff --git a/frontend-web/pages/admin/assets/js/views/settings.js b/frontend-web/pages/admin/assets/js/views/settings.js new file mode 100644 index 0000000..9528635 --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/settings.js @@ -0,0 +1,191 @@ +(function () { + 'use strict'; + + var Api = function () { return window.AdminApi; }; + var Ui = function () { return window.AdminUi; }; + var I = function () { return window.AdminI18n; }; + var Form = function () { return window.AdminForm; }; + var Tabs = function () { return window.AdminTabs; }; + var Filter = function () { return window.AdminFilter; }; + var State = function () { return window.AdminState; }; + var Router = function () { return window.AdminRouter; }; + + var SETTING_LABELS = { + 'site.base_url': 'API 站点外网地址', + 'site.front_base_url': '前端站点地址', + }; + + var valuesByKey = {}; + var allItems = []; + + function siteFields() { + var cfg = window.SOON_DEPLOY_CONFIG || {}; + var apiBase = String(cfg.api_v1_base || '').replace(/\/api\/v1\/?$/, ''); + var frontBase = cfg.front_base_url || cfg.web_base || ''; + return [ + { + key: 'site.base_url', + label: 'API 站点外网地址', + hint: '须 HTTPS 外网可达;支付宝/微信 notify 回调依赖此地址', + placeholder: apiBase, + }, + { + key: 'site.front_base_url', + label: '前端站点地址', + hint: '支付完成 return_url 跳转至会员页', + placeholder: frontBase, + }, + ]; + } + + function settingLabel(key) { + if (SETTING_LABELS[key]) { + return I().esc(key) + '
    ' + I().esc(SETTING_LABELS[key]) + ''; + } + return I().esc(key); + } + + function maskValue(key, val) { + if (/secret|password|key/i.test(key) && val) return '******'; + return I().esc(val); + } + + function siteFormHtml() { + var fields = siteFields().map(function (f) { + var val = valuesByKey[f.key] != null ? valuesByKey[f.key] : ''; + return Form().field(f.label, + Form().input('cfg_' + f.key.replace(/\./g, '_'), { value: val, placeholder: f.placeholder }), + f.hint); + }).join(''); + return '
    ' + Form().panel('站点与回调地址', fields) + + Form().actions('保存站点配置', 'settingsSiteSave') + '
    '; + } + + function allSettingsHtml(filterQ) { + var q = (filterQ || '').toLowerCase(); + var items = allItems.filter(function (s) { + if (!q) return true; + return String(s.key).toLowerCase().indexOf(q) >= 0 || + String(s.value || '').toLowerCase().indexOf(q) >= 0; + }); + var rows = items.map(function (s) { + return '' + settingLabel(s.key) + '' + maskValue(s.key, s.value) + '' + + I().formatDateTime(s.updated_at) + '' + + ''; + }).join(''); + var filter = Filter().bar( + Filter().field('搜索', Filter().searchInput('settingsSearchQ', filterQ, '按 key 或 value 搜索')), + '' + + '' + ); + return filter + Ui().dataTable(['配置项', '当前值', '更新时间', '操作'], rows); + } + + function bindSiteForm() { + Ui().bindFormSubmit('settingsSiteSave', function () { + var items = siteFields().map(function (f) { + var el = Ui().el('cfg_' + f.key.replace(/\./g, '_')); + return { key: f.key, value: el ? el.value.trim() : '' }; + }); + return Api().postJson('settings/batch', { items: items }).then(function (r) { + if (r) { + soonToast('站点配置已保存', 'success'); + render(); + } + }); + }); + } + + function bindAllSettings() { + var st = State().settings; + var searchBtn = Ui().el('settingsSearchBtn'); + var resetBtn = Ui().el('settingsSearchReset'); + if (searchBtn) { + searchBtn.onclick = function () { + st.q = Ui().el('settingsSearchQ').value.trim(); + st.tab = 'all'; + Router().syncQuery(); + render(); + }; + } + if (resetBtn) { + resetBtn.onclick = function () { + st.q = ''; + st.tab = 'all'; + Router().syncQuery(); + render(); + }; + } + var searchInput = Ui().el('settingsSearchQ'); + if (searchInput) { + searchInput.onkeydown = function (e) { + if (e.key === 'Enter' && searchBtn) searchBtn.click(); + }; + } + document.querySelectorAll('#panel-settings .setting-edit').forEach(function (btn) { + btn.onclick = function () { + var key = btn.dataset.key; + var val = valuesByKey[key] != null ? valuesByKey[key] : ''; + var form = '
    ' + + Form().field('配置键', '') + + Form().field('配置值', Form().input('settingVal', { value: val, type: 'textarea', rows: 5 })) + + '
    '; + Ui().openFormModal('修改 ' + key, form, function () { + Ui().confirm('确认保存该配置?', function () { + var saveBtn = Ui().el('adminModalSave'); + Ui().setBtnLoading(saveBtn, true); + Api().postJson('settings', { key: key, value: Ui().el('settingVal').value }).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('配置已保存', 'success'); + render(); + } + }).finally(function () { + Ui().setBtnLoading(saveBtn, false); + }); + }); + }); + }; + }); + } + + function render() { + var panel = Ui().el('panel-settings'); + if (!panel) return; + var st = State().settings; + var header = Ui().pageHeader('系统设置', '站点地址与运行时参数;修改后即时生效'); + panel.innerHTML = '
    ' + header + Ui().skeletonTable() + '
    '; + + Api().get('settings').then(function (j) { + if (!j) { + panel.innerHTML = '
    ' + header + + Ui().listError('设置加载失败', 'settingsRetry') + '
    '; + Ui().el('settingsRetry').onclick = render; + return; + } + valuesByKey = {}; + allItems = j.data.items || []; + allItems.forEach(function (s) { valuesByKey[s.key] = s.value; }); + + var tabsHtml = Tabs().html('settingsTabs', [ + { id: 'site', label: '站点配置' }, + { id: 'all', label: '全部配置' }, + ], { + site: siteFormHtml(), + all: allSettingsHtml(st.q), + }, st.tab); + + panel.innerHTML = '
    ' + header + tabsHtml + '
    '; + Tabs().bind(Ui().el('settingsTabs'), function (tabId) { + st.tab = tabId; + Router().syncQuery(); + }); + bindSiteForm(); + bindAllSettings(); + }); + } + + window.AdminViews = window.AdminViews || {}; + window.AdminViews.settings = { render: render }; +})(); diff --git a/frontend-web/pages/admin/assets/js/views/users.js b/frontend-web/pages/admin/assets/js/views/users.js new file mode 100644 index 0000000..6e8d13e --- /dev/null +++ b/frontend-web/pages/admin/assets/js/views/users.js @@ -0,0 +1,417 @@ +(function () { + 'use strict'; + + var Api = function () { return window.AdminApi; }; + var Ui = function () { return window.AdminUi; }; + var I = function () { return window.AdminI18n; }; + var State = function () { return window.AdminState; }; + var Filter = function () { return window.AdminFilter; }; + var Menu = function () { return window.AdminMenu; }; + var Router = function () { return window.AdminRouter; }; + + function buildQuery() { + var st = State().users; + var q = 'users?page=' + st.page + '&size=' + st.size; + if (st.q) q += '&q=' + encodeURIComponent(st.q); + return q; + } + + function formField(label, inner) { + return '
    ' + inner + '
    '; + } + + function roleOptions(selected) { + var roles = ['user']; + if (State().adminLevel === 'full') roles.push('admin'); + return roles.map(function (r) { + return ''; + }).join(''); + } + + function adminLevelOptions(selected) { + return [ + { v: 'full', l: '超级管理员' }, + { v: 'ops', l: '运营管理员' }, + ].map(function (o) { + return ''; + }).join(''); + } + + function bindAdminLevelField() { + var roleEl = Ui().el('ueRole') || Ui().el('ucRole'); + var wrap = Ui().el('adminLevelField'); + if (!roleEl || !wrap) return; + function sync() { + wrap.style.display = roleEl.value === 'admin' ? '' : 'none'; + } + roleEl.onchange = sync; + sync(); + } + + function showCreateModal() { + var form = '
    ' + + formField('邮箱', '') + + formField('密码', '') + + formField('角色', '') + + (State().adminLevel === 'full' + ? formField('管理级别', '', '运营管理员不可访问设置、审计与支付密钥') + : '') + + '
    '; + Ui().openFormModal('新增用户', form, function () { + var email = Ui().el('ucEmail').value.trim(); + var pwd = Ui().el('ucPwd').value; + if (!email || pwd.length < 8) { + soonToast('请填写有效邮箱与至少 8 位密码', 'warn'); + return false; + } + var body = { + email: email, + password: pwd, + role: Ui().el('ucRole').value, + }; + if (body.role === 'admin') { + var lv = Ui().el('ucAdminLevel'); + body.admin_level = lv ? lv.value : 'ops'; + } + return Api().postJson('users', body).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('用户已创建', 'success'); + render(); + } + }); + }, { + success: function () { bindAdminLevelField(); }, + }); + } + + function showEditModal(user) { + var level = user.admin_level || 'full'; + var form = '
    ' + + formField('邮箱', '') + + formField('角色', '') + + (State().adminLevel === 'full' + ? formField('管理级别', '
    ', '仅对管理员账号生效') + : '') + + '
    '; + Ui().openFormModal('编辑用户 #' + user.id, form, function () { + var body = { + email: Ui().el('ueEmail').value.trim(), + role: Ui().el('ueRole').value, + }; + if (body.role === 'admin') { + var lv = Ui().el('ueAdminLevel'); + if (lv) body.admin_level = lv.value; + } + return Api().putJson('users/' + user.id, body).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('用户信息已更新', 'success'); + render(); + } + }); + }, { + success: function () { bindAdminLevelField(); }, + }); + } + + function showPasswordModal(userId) { + var form = '
    ' + + formField('新密码', '') + + formField('确认密码', '') + + '
    '; + Ui().openFormModal('重置密码', form, function () { + var p1 = Ui().el('upPwd').value; + var p2 = Ui().el('upPwd2').value; + if (p1.length < 8) { + soonToast('密码至少 8 位', 'warn'); + return false; + } + if (p1 !== p2) { + soonToast('两次密码不一致', 'warn'); + return false; + } + return Api().postJson('users/' + userId + '/password', { password: p1 }).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('密码已重置', 'success'); + } + }); + }); + } + + function showSubscriptionModal(userId) { + Api().get('plans').then(function (j) { + if (!j) return; + var opts = (j.data.items || []).map(function (p) { + return ''; + }).join(''); + var form = '
    ' + + formField('套餐', '') + + formField('有效天数', '') + + '

    选择免费版将取消当前有效会员。

    ' + + '
    '; + Ui().openFormModal('设置会员', form, function () { + var planSel = Ui().el('usPlan'); + var daysVal = Ui().el('usDays').value.trim(); + var body = { plan_id: parseInt(planSel.value, 10) }; + if (daysVal) body.days = parseInt(daysVal, 10); + return Api().postJson('users/' + userId + '/subscription', body).then(function (r) { + if (r) { + layer.closeAll(); + soonToast('会员已更新', 'success'); + render(); + } + }); + }); + var planEl = Ui().el('usPlan'); + if (planEl) { + planEl.onchange = function () { + var opt = planEl.options[planEl.selectedIndex]; + Ui().el('usDays').placeholder = '默认 ' + (opt.getAttribute('data-days') || '0') + ' 天'; + }; + planEl.onchange(); + } + }); + } + + function showUserDetail(id, opts) { + opts = opts || {}; + if (!opts.fromRoute) Router().openDetail(id); + Api().get('users/' + id).then(function (j) { + if (!j) return; + var d = j.data; + var u = d.user || {}; + var sub = d.subscription; + var usage = d.usage || {}; + var adminId = State().currentAdminId; + + var levelLabel = u.role === 'admin' + ? (u.admin_level === 'ops' ? '运营管理员' : '超级管理员') + : '—'; + var account = Ui().detailRow('邮箱', I().esc(u.email)) + + Ui().detailRow('角色', I().esc(I().tRole(u.role))) + + (u.role === 'admin' ? Ui().detailRow('管理级别', I().esc(levelLabel)) : '') + + Ui().detailRow('状态', I().badgeStatus(u.status)) + + Ui().detailRow('注册时间', I().esc(I().formatDateTime(u.created_at))); + + var subHtml; + if (sub && sub.plan_name) { + subHtml = Ui().detailRow('套餐', I().esc(sub.plan_name)) + + Ui().detailRow('状态', I().badgeStatus(sub.status)) + + Ui().detailRow('开始时间', I().esc(I().formatDateTime(sub.started_at))) + + Ui().detailRow('到期时间', I().esc(I().formatDateTime(sub.expires_at))); + } else { + subHtml = '

    免费版 / 无有效订阅

    '; + } + + var usageHtml = Ui().detailRow('文件数', I().esc(usage.files_count || 0)) + + Ui().detailRow('占用空间', I().esc(I().formatBytes(usage.storage_bytes))); + + var orderRows = (d.recent_orders || []).map(function (o) { + return '' + I().esc(o.order_no || o.id) + '' + I().formatCents(o.amount_cents) + + '' + I().badgeStatus(o.status) + '' + I().formatDateTime(o.created_at) + ''; + }).join(''); + var ordersHtml = Ui().dataTable(['订单号', '金额', '状态', '时间'], orderRows, 4); + + var actions = '
    ' + + '' + + '' + + '' + + (u.id !== adminId ? '' : '') + + '
    '; + + Ui().openDrawer('用户 #' + u.id, actions + + Ui().detailSection('账号信息', account) + + Ui().detailSection('当前会员', subHtml) + + Ui().detailSection('云端用量', usageHtml) + + Ui().detailSection('最近订单', ordersHtml)); + + Ui().el('drawerEdit').onclick = function () { Ui().closeDrawer(); showEditModal(u); }; + Ui().el('drawerPwd').onclick = function () { showPasswordModal(u.id); }; + Ui().el('drawerSub').onclick = function () { showSubscriptionModal(u.id); }; + var delBtn = Ui().el('drawerDel'); + if (delBtn) { + delBtn.onclick = function () { + Ui().confirm('确认删除该用户?其云端文件将标记删除且不可恢复。', function () { + Api().del('users/' + u.id).then(function (r) { + if (r) { + Ui().closeDrawer(); + soonToast('用户已删除', 'success'); + render(); + } + }); + }); + }; + } + }); + } + + function userRowActions(u, adminId) { + var isSelf = adminId && u.id === adminId; + var editDisabled = State().adminLevel === 'ops' && u.role === 'admin'; + var ds = { id: String(u.id) }; + var editDs = { + id: String(u.id), + email: u.email, + role: u.role, + admin_level: u.admin_level || 'full', + }; + var inline = [ + { label: '详情', action: 'detail', dataset: ds }, + { label: '编辑', action: 'edit', dataset: editDs, disabled: editDisabled }, + { label: '订阅', action: 'sub', dataset: ds }, + ]; + var more = [ + { label: '改密', action: 'pwd', dataset: ds }, + ]; + if (u.status === 'disabled') { + more.push({ label: '启用', action: 'status', dataset: { id: String(u.id), st: 'active' } }); + } else if (!isSelf) { + more.push({ label: '停用', action: 'status', dataset: { id: String(u.id), st: 'disabled' } }); + } else { + more.push({ label: '停用', action: 'status', disabled: true }); + } + if (!isSelf) { + more.push({ + label: '删除', + action: 'delete', + className: 'soon-admin-action-menu__item--danger', + dataset: ds, + }); + } + return Menu().rowActions('user-' + u.id, inline, more); + } + + function bindUserMenus(panel, adminId) { + Menu().bind(panel, function (action, btn) { + var id = parseInt(btn.dataset.id, 10); + if (action === 'detail') showUserDetail(id); + if (action === 'edit') { + showEditModal({ + id: id, + email: btn.dataset.email, + role: btn.dataset.role, + admin_level: btn.dataset.adminLevel || 'full', + }); + } + if (action === 'pwd') showPasswordModal(id); + if (action === 'sub') showSubscriptionModal(id); + if (action === 'status' && btn.dataset.st) { + Ui().confirm('确认变更用户状态?', function () { + Api().postJson('users/' + id + '/status', { status: btn.dataset.st }).then(function (r) { + if (r) { + soonToast('状态已更新', 'success'); + render(); + } + }); + }); + } + if (action === 'delete') { + Ui().confirm('确认删除该用户?其云端文件将标记删除。', function () { + Api().del('users/' + id).then(function (r) { + if (r) { + soonToast('用户已删除', 'success'); + render(); + } + }); + }); + } + }); + } + + function renderFilterBar(st) { + return Filter().bar( + Filter().field('邮箱', Filter().searchInput('userSearchQ', st.q, '搜索邮箱')), + '' + + '' + ); + } + + function bindFilters(st) { + Ui().el('userSearchBtn').onclick = function () { + st.q = Ui().el('userSearchQ').value.trim(); + st.page = 1; + Router().syncQuery(); + render(); + }; + var reset = Ui().el('userSearchReset'); + if (reset) { + reset.onclick = function () { + st.q = ''; + st.page = 1; + Router().syncQuery(); + render(); + }; + } + Ui().el('userSearchQ').onkeydown = function (e) { + if (e.key === 'Enter') { + st.q = Ui().el('userSearchQ').value.trim(); + st.page = 1; + Router().syncQuery(); + render(); + } + }; + } + + function render() { + var panel = Ui().el('panel-users'); + if (!panel) return; + if (!Router().hasPendingDetail()) Ui().closeDrawer(); + var st = State().users; + var pageShell = '
    ' + + Ui().pageHeader('用户管理', '增删改用户、重置密码与设置会员', + '') + + renderFilterBar(st) + Ui().skeletonTable() + '
    '; + panel.innerHTML = pageShell; + + Api().get(buildQuery()).then(function (j) { + if (!j) { + panel.innerHTML = '
    ' + + Ui().pageHeader('用户管理', '增删改用户、重置密码与设置会员', + '') + + renderFilterBar(st) + Ui().listError('用户列表加载失败', 'usersListRetry') + '
    '; + Ui().el('userCreateBtn').onclick = showCreateModal; + Ui().el('usersListRetry').onclick = render; + bindFilters(st); + var errDetailId = Router().consumeDetailId(); + if (errDetailId) showUserDetail(errDetailId, { fromRoute: true }); + return; + } + var items = j.data.items || []; + var adminId = State().currentAdminId; + var rows = items.map(function (u) { + var planLabel = u.plan_name ? I().esc(u.plan_name) : '免费版'; + return '' + u.id + '' + I().esc(u.email) + '' + I().esc(I().tRole(u.role)) + + '' + planLabel + '' + I().badgeStatus(u.status) + '' + + I().formatDateTime(u.created_at) + '' + + userRowActions(u, adminId) + ''; + }).join(''); + + panel.innerHTML = '
    ' + + Ui().pageHeader('用户管理', '增删改用户、重置密码与设置会员', + '') + + renderFilterBar(st) + + Ui().dataTable(['ID', '邮箱', '角色', '会员', '状态', '注册时间', '操作'], rows) + + Ui().pagerHtml(j.data.page, j.data.size, j.data.total) + '
    '; + + Ui().el('userCreateBtn').onclick = showCreateModal; + bindFilters(st); + Ui().bindPager(panel, j.data.page, j.data.size, j.data.total, function (p, newSize) { + st.page = p; + if (newSize) st.size = newSize; + Router().syncQuery(); + render(); + }); + bindUserMenus(panel, adminId); + var detailId = Router().consumeDetailId(); + if (detailId) showUserDetail(detailId, { fromRoute: true }); + }); + } + + window.AdminViews = window.AdminViews || {}; + window.AdminViews.users = { render: render }; +})(); diff --git a/frontend-web/pages/admin/index.html b/frontend-web/pages/admin/index.html new file mode 100644 index 0000000..b277a88 --- /dev/null +++ b/frontend-web/pages/admin/index.html @@ -0,0 +1,63 @@ + + + + + + + + SoonDesign 管理后台 + + + + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend-web/pages/admin/login.html b/frontend-web/pages/admin/login.html new file mode 100644 index 0000000..900bec1 --- /dev/null +++ b/frontend-web/pages/admin/login.html @@ -0,0 +1,50 @@ + + + + + + + + 管理后台登录 · SoonDesign + + + + + +
    +
    + SoonDesign Admin +
    +
    +
    +
    +
    + +

    管理后台

    +

    仅限管理员账号登录

    +
    +
    +

    管理员登录

    +

    请使用管理员邮箱与密码

    +
    +
    + +
    +
    + + +
    + + +
    +
    +
    +
    + + + + + + + + diff --git a/design1.web.html b/frontend-web/pages/design1.web.html similarity index 86% rename from design1.web.html rename to frontend-web/pages/design1.web.html index 9d6077b..479153e 100644 --- a/design1.web.html +++ b/frontend-web/pages/design1.web.html @@ -4,9 +4,15 @@ + + Soon Design - - + + + + + + - + + +
    - - - + + +
    - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design2.web.html b/frontend-web/pages/design2.web.html similarity index 84% rename from design2.web.html rename to frontend-web/pages/design2.web.html index 12ad3a9..2dc8c95 100644 --- a/design2.web.html +++ b/frontend-web/pages/design2.web.html @@ -1,668 +1,697 @@ - - - - - - - Soon Design - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + Soon Design + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend-web/pages/index.web.html b/frontend-web/pages/index.web.html new file mode 100644 index 0000000..0987d1e --- /dev/null +++ b/frontend-web/pages/index.web.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + SoonDesign + + + + + + + + + + + + + +
    + + + +
    + +
    + +
    + + + +
    + +
    + +
    + +
    + +
    + +
    + + + + 0 + +
    + +

    + + + +
    + +
    + +
    + +
    + +
    + +
    + + + + ... + +
    + +
    + + + +
    + +
    + +
    + +
    + +
    + + + +
    + +
    + +
    + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend-web/pages/login.web.html b/frontend-web/pages/login.web.html new file mode 100644 index 0000000..1cf60d8 --- /dev/null +++ b/frontend-web/pages/login.web.html @@ -0,0 +1,64 @@ + + + + + + + + 登录 · SoonDesign + + + + +
    +
    +
    +
    + +

    SoonDesign

    +

    在线卡证设计

    +
    +
    +

    登录账号

    +

    登录后同步云端文件

    +
    +
    + + +
    +
    + + + +
    + + +
    + +
    +
    +
    + + + + + + + + + diff --git a/frontend-web/pages/member.web.html b/frontend-web/pages/member.web.html new file mode 100644 index 0000000..37ff9b6 --- /dev/null +++ b/frontend-web/pages/member.web.html @@ -0,0 +1,71 @@ + + + + + + + + 订阅中心 · SoonDesign + + + + + + +
    支付成功,订阅已生效
    + +
    + +
    +
    +
    +

    订阅服务

    +

    订阅中心

    +

    免费体验设计工具;订阅后解锁预览、打印与云端交付

    +
    +
    + +
    +
    +
    + +
    +
    +

    订阅方案

    +

    选择适合您的订阅周期,功能相同,随时续订

    +
    +
    +
    +
    +
    + +
    +
    +

    订单记录

    +

    订阅与支付记录,支持分页浏览

    +
    +
    +
    加载中...
    +
    +
    +
    + + + + + + + + + + + + + + + diff --git a/frontend-web/pages/register.web.html b/frontend-web/pages/register.web.html new file mode 100644 index 0000000..ba757c1 --- /dev/null +++ b/frontend-web/pages/register.web.html @@ -0,0 +1,64 @@ + + + + + + + + 注册 · SoonDesign + + + + +
    +
    +
    +
    + +

    加入 SoonDesign

    +

    注册后即可使用

    +
    +
    +

    创建账号

    +

    填写邮箱与密码完成注册

    +
    +
    + + +
    +
    + + + +
    + + +
    + +
    +
    +
    + + + + + + + + + diff --git a/frontend-web/vendor/js/JsBarcode.all.min.js b/frontend-web/vendor/js/JsBarcode.all.min.js new file mode 100644 index 0000000..bc710e1 --- /dev/null +++ b/frontend-web/vendor/js/JsBarcode.all.min.js @@ -0,0 +1,2 @@ +/*! JsBarcode v3.12.1 | (c) Johan Lindell | MIT license */ +!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=16)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.default=function t(e,n){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.data=e,this.text=n.text||e,this.options=n}},function(t,e,n){"use strict";var r;function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}Object.defineProperty(e,"__esModule",{value:!0});var i=e.SET_A=0,a=e.SET_B=1,u=e.SET_C=2,f=(e.SHIFT=98,e.START_A=103),c=e.START_B=104,s=e.START_C=105;e.MODULO=103,e.STOP=106,e.FNC1=207,e.SET_BY_CODE=(o(r={},f,i),o(r,c,a),o(r,s,u),r),e.SWAP={101:i,100:a,99:u},e.A_START_CHAR=String.fromCharCode(208),e.B_START_CHAR=String.fromCharCode(209),e.C_START_CHAR=String.fromCharCode(210),e.A_CHARS="[\0-_È-Ï]",e.B_CHARS="[ -È-Ï]",e.C_CHARS="(Ï*[0-9]{2}Ï*)",e.BARS=[11011001100,11001101100,11001100110,10010011e3,10010001100,10001001100,10011001e3,10011000100,10001100100,11001001e3,11001000100,11000100100,10110011100,10011011100,10011001110,10111001100,10011101100,10011100110,11001110010,11001011100,11001001110,11011100100,11001110100,11101101110,11101001100,11100101100,11100100110,11101100100,11100110100,11100110010,11011011e3,11011000110,11000110110,10100011e3,10001011e3,10001000110,10110001e3,10001101e3,10001100010,11010001e3,11000101e3,11000100010,10110111e3,10110001110,10001101110,10111011e3,10111000110,10001110110,11101110110,11010001110,11000101110,11011101e3,11011100010,11011101110,11101011e3,11101000110,11100010110,11101101e3,11101100010,11100011010,11101111010,11001000010,11110001010,1010011e4,10100001100,1001011e4,10010000110,10000101100,10000100110,1011001e4,10110000100,1001101e4,10011000010,10000110100,10000110010,11000010010,1100101e4,11110111010,11000010100,10001111010,10100111100,10010111100,10010011110,10111100100,10011110100,10011110010,11110100100,11110010100,11110010010,11011011110,11011110110,11110110110,10101111e3,10100011110,10001011110,10111101e3,10111100010,11110101e3,11110100010,10111011110,10111101110,11101011110,11110101110,11010000100,1101001e4,11010011100,1100011101011]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.SIDE_BIN="101",e.MIDDLE_BIN="01010",e.BINARIES={L:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],G:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"],R:["1110010","1100110","1101100","1000010","1011100","1001110","1010000","1000100","1001000","1110100"],O:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],E:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"]},e.EAN2_STRUCTURE=["LL","LG","GL","GG"],e.EAN5_STRUCTURE=["GGLLL","GLGLL","GLLGL","GLLLG","LGGLL","LLGGL","LLLGG","LGLGL","LGLLG","LLGLG"],e.EAN13_STRUCTURE=["LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG","LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e.default=function(t,e,n){var o=t.split("").map((function(t,n){return r.BINARIES[e[n]]})).map((function(e,n){return e?e[t[n]]:""}));if(n){var i=t.length-1;o=o.map((function(t,e){return e=200){i=t.shift()-105;var a=u.SWAP[i];void 0!==a?o=e.next(t,n+1,a):(r!==u.SET_A&&r!==u.SET_B||i!==u.SHIFT||(t[0]=r===u.SET_A?t[0]>95?t[0]-96:t[0]:t[0]<32?t[0]+96:t[0]),o=e.next(t,n+1,r))}else i=e.correctIndex(t,r),o=e.next(t,n+1,r);var f=i*n;return{result:e.getBar(i)+o.result,checksum:f+o.checksum}}}]),e}(a.default);e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.mod10=function(t){for(var e=0,n=0;n10*n.width?10*n.width:n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"encode",value:function(){return this.options.flat?this.encodeFlat():this.encodeGuarded()}},{key:"leftText",value:function(t,e){return this.text.substr(t,e)}},{key:"leftEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"rightText",value:function(t,e){return this.text.substr(t,e)}},{key:"rightEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"encodeGuarded",value:function(){var t={fontSize:this.fontSize},e={height:this.guardHeight};return[{data:o.SIDE_BIN,options:e},{data:this.leftEncode(),text:this.leftText(),options:t},{data:o.MIDDLE_BIN,options:e},{data:this.rightEncode(),text:this.rightText(),options:t},{data:o.SIDE_BIN,options:e}]}},{key:"encodeFlat",value:function(){return{data:[o.SIDE_BIN,this.leftEncode(),o.MIDDLE_BIN,this.rightEncode(),o.SIDE_BIN].join(""),text:this.text}}}]),e}(a(n(0)).default);e.default=u},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return-1!==this.data.search(/^[0-9]{12}$/)&&this.data[11]==u(this.data)}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=(0,o.default)(this.data.substr(0,6),"LLLLLL"),t+="01010",t+=(0,o.default)(this.data.substr(6,6),"RRRRRR"),{data:t+="101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text.substr(0,1),options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101"+(0,o.default)(this.data[0],"L"),options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(1,5),"LLLLL"),text:this.text.substr(1,5),options:{fontSize:this.fontSize}}),t.push({data:"01010",options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(6,5),"RRRRR"),text:this.text.substr(6,5),options:{fontSize:this.fontSize}}),t.push({data:(0,o.default)(this.data[11],"R")+"101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text.substr(11,1),options:{textAlign:"right",fontSize:this.fontSize}}),t}}]),e}(i(n(0)).default);function u(t){var e,n=0;for(e=1;e<11;e+=2)n+=parseInt(t[e]);for(e=0;e<11;e+=2)n+=3*parseInt(t[e]);return(10-n%10)%10}e.default=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?e.fontSize+e.textMargin:0)+e.marginTop+e.marginBottom}function u(t,e,n){if(n.displayValue&&ee&&(e=t[n].height);return e},e.getEncodingHeight=a,e.getBarcodePadding=u,e.calculateEncodingAttributes=function(t,e,n){for(var r=0;r=i(t);return e+String.fromCharCode(r?206:205)+u(t,r)}e.default=function(t){var e=void 0;if(a(t).length>=2)e=r.C_START_CHAR+f(t);else{var n=o(t)>i(t);e=(n?r.A_START_CHAR:r.B_START_CHAR)+u(t,n)}return e.replace(/[\xCD\xCE]([^])[\xCD\xCE]/,(function(t,e){return String.fromCharCode(203)+e}))}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return this.isValid}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=this.encodeMiddleDigits(),{data:t+="010101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text[0],options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101",options:{height:this.guardHeight}}),t.push({data:this.encodeMiddleDigits(),text:this.text.substring(1,7),options:{fontSize:this.fontSize}}),t.push({data:"010101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text[7],options:{textAlign:"right",fontSize:this.fontSize}}),t}},{key:"encodeMiddleDigits",value:function(){var t=this.upcA[0],e=this.upcA[this.upcA.length-1],n=s[parseInt(e)][parseInt(t)];return(0,o.default)(this.middleDigits,n)}}]),e}(i.default);function p(t,e){for(var n=parseInt(t[t.length-1]),r=c[n],o="",i=0,u=0;u=3&&this.number<=131070}}]),e}(((r=i)&&r.__esModule?r:{default:r}).default);e.pharmacode=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.codabar=void 0;var r,o=function(){function t(t,e){for(var n=0;n":["(%)","I"],"?":["(%)","J"],"@":["(%)","V"],"[":["(%)","K"],"\\":["(%)","L"],"]":["(%)","M"],"^":["(%)","N"],_:["(%)","O"],"`":["(%)","W"],a:["(+)","A"],b:["(+)","B"],c:["(+)","C"],d:["(+)","D"],e:["(+)","E"],f:["(+)","F"],g:["(+)","G"],h:["(+)","H"],i:["(+)","I"],j:["(+)","J"],k:["(+)","K"],l:["(+)","L"],m:["(+)","M"],n:["(+)","N"],o:["(+)","O"],p:["(+)","P"],q:["(+)","Q"],r:["(+)","R"],s:["(+)","S"],t:["(+)","T"],u:["(+)","U"],v:["(+)","V"],w:["(+)","W"],x:["(+)","X"],y:["(+)","Y"],z:["(+)","Z"],"{":["(%)","P"],"|":["(%)","Q"],"}":["(%)","R"],"~":["(%)","S"],"":["(%)","T"]}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?(n=0,o.textAlign="left"):"right"==t.textAlign?(n=e.width-1,o.textAlign="right"):(n=e.width/2,o.textAlign="center"),o.fillText(e.text,n,r))}},{key:"moveCanvasDrawing",value:function(t){this.canvas.getContext("2d").translate(t.width,0)}},{key:"restoreCanvas",value:function(){this.canvas.getContext("2d").restore()}}]),t}();e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0&&(this.drawRect(a-e.width*i,r,e.width*i,e.height,t),i=0);i>0&&this.drawRect(a-e.width*(i-1),r,e.width*i,e.height,t)}},{key:"drawSVGText",value:function(t,e,n){var r,o,i=this.document.createElementNS(f,"text");e.displayValue&&(i.setAttribute("style","font:"+e.fontOptions+" "+e.fontSize+"px "+e.font),o="top"==e.textPosition?e.fontSize-e.textMargin:e.height+e.textMargin+e.fontSize,"left"==e.textAlign||n.barcodePadding>0?(r=0,i.setAttribute("text-anchor","start")):"right"==e.textAlign?(r=n.width-1,i.setAttribute("text-anchor","end")):(r=n.width/2,i.setAttribute("text-anchor","middle")),i.setAttribute("x",r),i.setAttribute("y",o),i.appendChild(this.document.createTextNode(n.text)),t.appendChild(i))}},{key:"setSvgAttributes",value:function(t,e){var n=this.svg;n.setAttribute("width",t+"px"),n.setAttribute("height",e+"px"),n.setAttribute("x","0px"),n.setAttribute("y","0px"),n.setAttribute("viewBox","0 0 "+t+" "+e),n.setAttribute("xmlns",f),n.setAttribute("version","1.1"),n.setAttribute("style","transform: translate(0,0)")}},{key:"createGroup",value:function(t,e,n){var r=this.document.createElementNS(f,"g");return r.setAttribute("transform","translate("+t+", "+e+")"),n.appendChild(r),r}},{key:"setGroupOptions",value:function(t,e){t.setAttribute("style","fill:"+e.lineColor+";")}},{key:"drawRect",value:function(t,e,n,r,o){var i=this.document.createElementNS(f,"rect");return i.setAttribute("x",t),i.setAttribute("y",e),i.setAttribute("width",n),i.setAttribute("height",r),o.appendChild(i),i}}]),t}();e.default=c},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n>>2]>>>24-o%4*8&255;r[i+o>>>2]|=s<<24-(i+o)%4*8}else for(var o=0;o>>2]=e[o>>>2];return this.sigBytes+=n,this},clamp:function(){var r=this.words,e=this.sigBytes;r[e>>>2]&=4294967295<<32-e%4*8,r.length=t.ceil(e/4)},clone:function(){var t=o.clone.call(this);return t.words=this.words.slice(0),t},random:function(r){for(var e,i=[],n=function(r){var r=r,e=987654321,i=4294967295;return function(){e=36969*(65535&e)+(e>>16)&i,r=18e3*(65535&r)+(r>>16)&i;var n=(e<<16)+r&i;return n/=4294967296,n+=.5,n*(t.random()>.5?1:-1)}},o=0;o>>2]>>>24-n%4*8&255;i.push((o>>>4).toString(16)),i.push((15&o).toString(16))}return i.join("")},parse:function(t){for(var r=t.length,e=[],i=0;i>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new s.init(e,r/2)}},h=a.Latin1={stringify:function(t){for(var r=t.words,e=t.sigBytes,i=[],n=0;n>>2]>>>24-n%4*8&255;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var r=t.length,e=[],i=0;i>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new s.init(e,r)}},l=a.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},f=n.BufferedBlockAlgorithm=o.extend({reset:function(){this._data=new s.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=l.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(r){var e=this._data,i=e.words,n=e.sigBytes,o=this.blockSize,a=4*o,c=n/a;c=r?t.ceil(c):t.max((0|c)-this._minBufferSize,0);var h=c*o,l=t.min(4*h,n);if(h){for(var f=0;f>>6-s%4*2;i[o>>>2]|=(a|c)<<24-o%4*8,o++}return n.create(i,o)}var e=t,i=e.lib,n=i.WordArray,o=e.enc;o.Base64={stringify:function(t){var r=t.words,e=t.sigBytes,i=this._map;t.clamp();for(var n=[],o=0;o>>2]>>>24-o%4*8&255,a=r[o+1>>>2]>>>24-(o+1)%4*8&255,c=r[o+2>>>2]>>>24-(o+2)%4*8&255,h=s<<16|a<<8|c,l=0;l<4&&o+.75*l>>6*(3-l)&63));var f=i.charAt(64);if(f)for(;n.length%4;)n.push(f);return n.join("")},parse:function(t){var e=t.length,i=this._map,n=this._reverseMap;if(!n){n=this._reverseMap=[];for(var o=0;o>>32-o)+r}function i(t,r,e,i,n,o,s){var a=t+(r&i|e&~i)+n+s;return(a<>>32-o)+r}function n(t,r,e,i,n,o,s){var a=t+(r^e^i)+n+s;return(a<>>32-o)+r}function o(t,r,e,i,n,o,s){var a=t+(e^(r|~i))+n+s;return(a<>>32-o)+r}var s=t,a=s.lib,c=a.WordArray,h=a.Hasher,l=s.algo,f=[];!function(){for(var t=0;t<64;t++)f[t]=4294967296*r.abs(r.sin(t+1))|0}();var u=l.MD5=h.extend({_doReset:function(){this._hash=new c.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(t,r){for(var s=0;s<16;s++){var a=r+s,c=t[a];t[a]=16711935&(c<<8|c>>>24)|4278255360&(c<<24|c>>>8)}var h=this._hash.words,l=t[r+0],u=t[r+1],d=t[r+2],v=t[r+3],p=t[r+4],_=t[r+5],y=t[r+6],g=t[r+7],B=t[r+8],w=t[r+9],k=t[r+10],S=t[r+11],m=t[r+12],x=t[r+13],b=t[r+14],H=t[r+15],z=h[0],A=h[1],C=h[2],D=h[3];z=e(z,A,C,D,l,7,f[0]),D=e(D,z,A,C,u,12,f[1]),C=e(C,D,z,A,d,17,f[2]),A=e(A,C,D,z,v,22,f[3]),z=e(z,A,C,D,p,7,f[4]),D=e(D,z,A,C,_,12,f[5]),C=e(C,D,z,A,y,17,f[6]),A=e(A,C,D,z,g,22,f[7]),z=e(z,A,C,D,B,7,f[8]),D=e(D,z,A,C,w,12,f[9]),C=e(C,D,z,A,k,17,f[10]),A=e(A,C,D,z,S,22,f[11]),z=e(z,A,C,D,m,7,f[12]),D=e(D,z,A,C,x,12,f[13]),C=e(C,D,z,A,b,17,f[14]),A=e(A,C,D,z,H,22,f[15]),z=i(z,A,C,D,u,5,f[16]),D=i(D,z,A,C,y,9,f[17]),C=i(C,D,z,A,S,14,f[18]),A=i(A,C,D,z,l,20,f[19]),z=i(z,A,C,D,_,5,f[20]),D=i(D,z,A,C,k,9,f[21]),C=i(C,D,z,A,H,14,f[22]),A=i(A,C,D,z,p,20,f[23]),z=i(z,A,C,D,w,5,f[24]),D=i(D,z,A,C,b,9,f[25]),C=i(C,D,z,A,v,14,f[26]),A=i(A,C,D,z,B,20,f[27]),z=i(z,A,C,D,x,5,f[28]),D=i(D,z,A,C,d,9,f[29]),C=i(C,D,z,A,g,14,f[30]),A=i(A,C,D,z,m,20,f[31]),z=n(z,A,C,D,_,4,f[32]),D=n(D,z,A,C,B,11,f[33]),C=n(C,D,z,A,S,16,f[34]),A=n(A,C,D,z,b,23,f[35]),z=n(z,A,C,D,u,4,f[36]),D=n(D,z,A,C,p,11,f[37]),C=n(C,D,z,A,g,16,f[38]),A=n(A,C,D,z,k,23,f[39]),z=n(z,A,C,D,x,4,f[40]),D=n(D,z,A,C,l,11,f[41]),C=n(C,D,z,A,v,16,f[42]),A=n(A,C,D,z,y,23,f[43]),z=n(z,A,C,D,w,4,f[44]),D=n(D,z,A,C,m,11,f[45]),C=n(C,D,z,A,H,16,f[46]),A=n(A,C,D,z,d,23,f[47]),z=o(z,A,C,D,l,6,f[48]),D=o(D,z,A,C,g,10,f[49]),C=o(C,D,z,A,b,15,f[50]),A=o(A,C,D,z,_,21,f[51]),z=o(z,A,C,D,m,6,f[52]),D=o(D,z,A,C,v,10,f[53]),C=o(C,D,z,A,k,15,f[54]),A=o(A,C,D,z,u,21,f[55]),z=o(z,A,C,D,B,6,f[56]),D=o(D,z,A,C,H,10,f[57]),C=o(C,D,z,A,y,15,f[58]),A=o(A,C,D,z,x,21,f[59]),z=o(z,A,C,D,p,6,f[60]),D=o(D,z,A,C,S,10,f[61]),C=o(C,D,z,A,d,15,f[62]),A=o(A,C,D,z,w,21,f[63]),h[0]=h[0]+z|0,h[1]=h[1]+A|0,h[2]=h[2]+C|0,h[3]=h[3]+D|0},_doFinalize:function(){var t=this._data,e=t.words,i=8*this._nDataBytes,n=8*t.sigBytes;e[n>>>5]|=128<<24-n%32;var o=r.floor(i/4294967296),s=i;e[(n+64>>>9<<4)+15]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),e[(n+64>>>9<<4)+14]=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),t.sigBytes=4*(e.length+1),this._process();for(var a=this._hash,c=a.words,h=0;h<4;h++){var l=c[h];c[h]=16711935&(l<<8|l>>>24)|4278255360&(l<<24|l>>>8)}return a},clone:function(){var t=h.clone.call(this);return t._hash=this._hash.clone(),t}});s.MD5=h._createHelper(u),s.HmacMD5=h._createHmacHelper(u)}(Math),function(){var r=t,e=r.lib,i=e.WordArray,n=e.Hasher,o=r.algo,s=[],a=o.SHA1=n.extend({_doReset:function(){this._hash=new i.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,r){for(var e=this._hash.words,i=e[0],n=e[1],o=e[2],a=e[3],c=e[4],h=0;h<80;h++){if(h<16)s[h]=0|t[r+h];else{var l=s[h-3]^s[h-8]^s[h-14]^s[h-16];s[h]=l<<1|l>>>31}var f=(i<<5|i>>>27)+c+s[h];f+=h<20?(n&o|~n&a)+1518500249:h<40?(n^o^a)+1859775393:h<60?(n&o|n&a|o&a)-1894007588:(n^o^a)-899497514,c=a,a=o,o=n<<30|n>>>2,n=i,i=f}e[0]=e[0]+i|0,e[1]=e[1]+n|0,e[2]=e[2]+o|0,e[3]=e[3]+a|0,e[4]=e[4]+c|0},_doFinalize:function(){var t=this._data,r=t.words,e=8*this._nDataBytes,i=8*t.sigBytes;return r[i>>>5]|=128<<24-i%32,r[(i+64>>>9<<4)+14]=Math.floor(e/4294967296),r[(i+64>>>9<<4)+15]=e,t.sigBytes=4*r.length,this._process(),this._hash},clone:function(){var t=n.clone.call(this);return t._hash=this._hash.clone(),t}});r.SHA1=n._createHelper(a),r.HmacSHA1=n._createHmacHelper(a)}(),function(r){var e=t,i=e.lib,n=i.WordArray,o=i.Hasher,s=e.algo,a=[],c=[];!function(){function t(t){for(var e=r.sqrt(t),i=2;i<=e;i++)if(!(t%i))return!1;return!0}function e(t){return 4294967296*(t-(0|t))|0}for(var i=2,n=0;n<64;)t(i)&&(n<8&&(a[n]=e(r.pow(i,.5))),c[n]=e(r.pow(i,1/3)),n++),i++}();var h=[],l=s.SHA256=o.extend({_doReset:function(){this._hash=new n.init(a.slice(0))},_doProcessBlock:function(t,r){for(var e=this._hash.words,i=e[0],n=e[1],o=e[2],s=e[3],a=e[4],l=e[5],f=e[6],u=e[7],d=0;d<64;d++){if(d<16)h[d]=0|t[r+d];else{var v=h[d-15],p=(v<<25|v>>>7)^(v<<14|v>>>18)^v>>>3,_=h[d-2],y=(_<<15|_>>>17)^(_<<13|_>>>19)^_>>>10;h[d]=p+h[d-7]+y+h[d-16]}var g=a&l^~a&f,B=i&n^i&o^n&o,w=(i<<30|i>>>2)^(i<<19|i>>>13)^(i<<10|i>>>22),k=(a<<26|a>>>6)^(a<<21|a>>>11)^(a<<7|a>>>25),S=u+k+g+c[d]+h[d],m=w+B;u=f,f=l,l=a,a=s+S|0,s=o,o=n,n=i,i=S+m|0}e[0]=e[0]+i|0,e[1]=e[1]+n|0,e[2]=e[2]+o|0,e[3]=e[3]+s|0,e[4]=e[4]+a|0,e[5]=e[5]+l|0,e[6]=e[6]+f|0,e[7]=e[7]+u|0},_doFinalize:function(){var t=this._data,e=t.words,i=8*this._nDataBytes,n=8*t.sigBytes;return e[n>>>5]|=128<<24-n%32,e[(n+64>>>9<<4)+14]=r.floor(i/4294967296),e[(n+64>>>9<<4)+15]=i,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=o.clone.call(this);return t._hash=this._hash.clone(),t}});e.SHA256=o._createHelper(l),e.HmacSHA256=o._createHmacHelper(l)}(Math),function(){function r(t){return t<<8&4278255360|t>>>8&16711935}var e=t,i=e.lib,n=i.WordArray,o=e.enc;o.Utf16=o.Utf16BE={stringify:function(t){for(var r=t.words,e=t.sigBytes,i=[],n=0;n>>2]>>>16-n%4*8&65535;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var r=t.length,e=[],i=0;i>>1]|=t.charCodeAt(i)<<16-i%2*16;return n.create(e,2*r)}};o.Utf16LE={stringify:function(t){for(var e=t.words,i=t.sigBytes,n=[],o=0;o>>2]>>>16-o%4*8&65535);n.push(String.fromCharCode(s))}return n.join("")},parse:function(t){for(var e=t.length,i=[],o=0;o>>1]|=r(t.charCodeAt(o)<<16-o%2*16);return n.create(i,2*e)}}}(),function(){if("function"==typeof ArrayBuffer){var r=t,e=r.lib,i=e.WordArray,n=i.init,o=i.init=function(t){if(t instanceof ArrayBuffer&&(t=new Uint8Array(t)),(t instanceof Int8Array||"undefined"!=typeof Uint8ClampedArray&&t instanceof Uint8ClampedArray||t instanceof Int16Array||t instanceof Uint16Array||t instanceof Int32Array||t instanceof Uint32Array||t instanceof Float32Array||t instanceof Float64Array)&&(t=new Uint8Array(t.buffer,t.byteOffset,t.byteLength)),t instanceof Uint8Array){for(var r=t.byteLength,e=[],i=0;i>>2]|=t[i]<<24-i%4*8;n.call(this,e,r)}else n.apply(this,arguments)};o.prototype=i}}(),function(r){function e(t,r,e){return t^r^e}function i(t,r,e){return t&r|~t&e}function n(t,r,e){return(t|~r)^e}function o(t,r,e){return t&e|r&~e}function s(t,r,e){return t^(r|~e)}function a(t,r){return t<>>32-r}var c=t,h=c.lib,l=h.WordArray,f=h.Hasher,u=c.algo,d=l.create([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13]),v=l.create([5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11]),p=l.create([11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6]),_=l.create([8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11]),y=l.create([0,1518500249,1859775393,2400959708,2840853838]),g=l.create([1352829926,1548603684,1836072691,2053994217,0]),B=u.RIPEMD160=f.extend({_doReset:function(){this._hash=l.create([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,r){for(var c=0;c<16;c++){var h=r+c,l=t[h];t[h]=16711935&(l<<8|l>>>24)|4278255360&(l<<24|l>>>8)}var f,u,B,w,k,S,m,x,b,H,z=this._hash.words,A=y.words,C=g.words,D=d.words,R=v.words,E=p.words,M=_.words;S=f=z[0],m=u=z[1],x=B=z[2],b=w=z[3],H=k=z[4];for(var F,c=0;c<80;c+=1)F=f+t[r+D[c]]|0,F+=c<16?e(u,B,w)+A[0]:c<32?i(u,B,w)+A[1]:c<48?n(u,B,w)+A[2]:c<64?o(u,B,w)+A[3]:s(u,B,w)+A[4],F|=0,F=a(F,E[c]),F=F+k|0,f=k,k=w,w=a(B,10),B=u,u=F,F=S+t[r+R[c]]|0,F+=c<16?s(m,x,b)+C[0]:c<32?o(m,x,b)+C[1]:c<48?n(m,x,b)+C[2]:c<64?i(m,x,b)+C[3]:e(m,x,b)+C[4],F|=0,F=a(F,M[c]),F=F+H|0,S=H,H=b,b=a(x,10),x=m,m=F;F=z[1]+B+b|0,z[1]=z[2]+w+H|0,z[2]=z[3]+k+S|0,z[3]=z[4]+f+m|0,z[4]=z[0]+u+x|0,z[0]=F},_doFinalize:function(){var t=this._data,r=t.words,e=8*this._nDataBytes,i=8*t.sigBytes;r[i>>>5]|=128<<24-i%32,r[(i+64>>>9<<4)+14]=16711935&(e<<8|e>>>24)|4278255360&(e<<24|e>>>8),t.sigBytes=4*(r.length+1),this._process();for(var n=this._hash,o=n.words,s=0;s<5;s++){var a=o[s];o[s]=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8)}return n},clone:function(){var t=f.clone.call(this);return t._hash=this._hash.clone(),t}});c.RIPEMD160=f._createHelper(B),c.HmacRIPEMD160=f._createHmacHelper(B)}(Math),function(){var r=t,e=r.lib,i=e.Base,n=r.enc,o=n.Utf8,s=r.algo;s.HMAC=i.extend({init:function(t,r){t=this._hasher=new t.init,"string"==typeof r&&(r=o.parse(r));var e=t.blockSize,i=4*e;r.sigBytes>i&&(r=t.finalize(r)),r.clamp();for(var n=this._oKey=r.clone(),s=this._iKey=r.clone(),a=n.words,c=s.words,h=0;h>>24)|4278255360&(o<<24|o>>>8),s=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8);var a=e[n];a.high^=s,a.low^=o}for(var c=0;c<24;c++){for(var d=0;d<5;d++){for(var v=0,p=0,_=0;_<5;_++){var a=e[d+5*_];v^=a.high,p^=a.low}var y=u[d];y.high=v,y.low=p}for(var d=0;d<5;d++)for(var g=u[(d+4)%5],B=u[(d+1)%5],w=B.high,k=B.low,v=g.high^(w<<1|k>>>31),p=g.low^(k<<1|w>>>31),_=0;_<5;_++){var a=e[d+5*_];a.high^=v,a.low^=p}for(var S=1;S<25;S++){var a=e[S],m=a.high,x=a.low,b=h[S];if(b<32)var v=m<>>32-b,p=x<>>32-b;else var v=x<>>64-b,p=m<>>64-b;var H=u[l[S]];H.high=v,H.low=p}var z=u[0],A=e[0];z.high=A.high,z.low=A.low;for(var d=0;d<5;d++)for(var _=0;_<5;_++){var S=d+5*_,a=e[S],C=u[S],D=u[(d+1)%5+5*_],R=u[(d+2)%5+5*_];a.high=C.high^~D.high&R.high,a.low=C.low^~D.low&R.low}var a=e[0],E=f[c];a.high^=E.high,a.low^=E.low}},_doFinalize:function(){var t=this._data,e=t.words,i=(8*this._nDataBytes,8*t.sigBytes),o=32*this.blockSize;e[i>>>5]|=1<<24-i%32,e[(r.ceil((i+1)/o)*o>>>5)-1]|=128,t.sigBytes=4*e.length,this._process();for(var s=this._state,a=this.cfg.outputLength/8,c=a/8,h=[],l=0;l>>24)|4278255360&(u<<24|u>>>8),d=16711935&(d<<8|d>>>24)|4278255360&(d<<24|d>>>8),h.push(d),h.push(u)}return new n.init(h,a)},clone:function(){for(var t=o.clone.call(this),r=t._state=this._state.slice(0),e=0;e<25;e++)r[e]=r[e].clone();return t}});e.SHA3=o._createHelper(d),e.HmacSHA3=o._createHmacHelper(d)}(Math),function(){function r(){return s.create.apply(s,arguments)}var e=t,i=e.lib,n=i.Hasher,o=e.x64,s=o.Word,a=o.WordArray,c=e.algo,h=[r(1116352408,3609767458),r(1899447441,602891725),r(3049323471,3964484399),r(3921009573,2173295548),r(961987163,4081628472),r(1508970993,3053834265),r(2453635748,2937671579),r(2870763221,3664609560),r(3624381080,2734883394),r(310598401,1164996542),r(607225278,1323610764),r(1426881987,3590304994),r(1925078388,4068182383),r(2162078206,991336113),r(2614888103,633803317),r(3248222580,3479774868),r(3835390401,2666613458),r(4022224774,944711139),r(264347078,2341262773),r(604807628,2007800933),r(770255983,1495990901),r(1249150122,1856431235),r(1555081692,3175218132),r(1996064986,2198950837),r(2554220882,3999719339),r(2821834349,766784016),r(2952996808,2566594879),r(3210313671,3203337956),r(3336571891,1034457026),r(3584528711,2466948901),r(113926993,3758326383),r(338241895,168717936),r(666307205,1188179964),r(773529912,1546045734),r(1294757372,1522805485),r(1396182291,2643833823),r(1695183700,2343527390),r(1986661051,1014477480),r(2177026350,1206759142),r(2456956037,344077627),r(2730485921,1290863460),r(2820302411,3158454273),r(3259730800,3505952657),r(3345764771,106217008),r(3516065817,3606008344),r(3600352804,1432725776),r(4094571909,1467031594),r(275423344,851169720),r(430227734,3100823752),r(506948616,1363258195),r(659060556,3750685593),r(883997877,3785050280),r(958139571,3318307427),r(1322822218,3812723403),r(1537002063,2003034995),r(1747873779,3602036899),r(1955562222,1575990012),r(2024104815,1125592928),r(2227730452,2716904306),r(2361852424,442776044),r(2428436474,593698344),r(2756734187,3733110249),r(3204031479,2999351573),r(3329325298,3815920427),r(3391569614,3928383900),r(3515267271,566280711),r(3940187606,3454069534),r(4118630271,4000239992),r(116418474,1914138554),r(174292421,2731055270),r(289380356,3203993006),r(460393269,320620315),r(685471733,587496836),r(852142971,1086792851),r(1017036298,365543100),r(1126000580,2618297676),r(1288033470,3409855158),r(1501505948,4234509866),r(1607167915,987167468),r(1816402316,1246189591)],l=[];!function(){for(var t=0;t<80;t++)l[t]=r()}();var f=c.SHA512=n.extend({_doReset:function(){this._hash=new a.init([new s.init(1779033703,4089235720),new s.init(3144134277,2227873595),new s.init(1013904242,4271175723),new s.init(2773480762,1595750129),new s.init(1359893119,2917565137),new s.init(2600822924,725511199),new s.init(528734635,4215389547),new s.init(1541459225,327033209)])},_doProcessBlock:function(t,r){for(var e=this._hash.words,i=e[0],n=e[1],o=e[2],s=e[3],a=e[4],c=e[5],f=e[6],u=e[7],d=i.high,v=i.low,p=n.high,_=n.low,y=o.high,g=o.low,B=s.high,w=s.low,k=a.high,S=a.low,m=c.high,x=c.low,b=f.high,H=f.low,z=u.high,A=u.low,C=d,D=v,R=p,E=_,M=y,F=g,P=B,W=w,O=k,U=S,I=m,K=x,X=b,L=H,j=z,N=A,T=0;T<80;T++){var Z=l[T];if(T<16)var q=Z.high=0|t[r+2*T],G=Z.low=0|t[r+2*T+1];else{var J=l[T-15],$=J.high,Q=J.low,V=($>>>1|Q<<31)^($>>>8|Q<<24)^$>>>7,Y=(Q>>>1|$<<31)^(Q>>>8|$<<24)^(Q>>>7|$<<25),tt=l[T-2],rt=tt.high,et=tt.low,it=(rt>>>19|et<<13)^(rt<<3|et>>>29)^rt>>>6,nt=(et>>>19|rt<<13)^(et<<3|rt>>>29)^(et>>>6|rt<<26),ot=l[T-7],st=ot.high,at=ot.low,ct=l[T-16],ht=ct.high,lt=ct.low,G=Y+at,q=V+st+(G>>>0>>0?1:0),G=G+nt,q=q+it+(G>>>0>>0?1:0),G=G+lt,q=q+ht+(G>>>0>>0?1:0);Z.high=q,Z.low=G}var ft=O&I^~O&X,ut=U&K^~U&L,dt=C&R^C&M^R&M,vt=D&E^D&F^E&F,pt=(C>>>28|D<<4)^(C<<30|D>>>2)^(C<<25|D>>>7),_t=(D>>>28|C<<4)^(D<<30|C>>>2)^(D<<25|C>>>7),yt=(O>>>14|U<<18)^(O>>>18|U<<14)^(O<<23|U>>>9),gt=(U>>>14|O<<18)^(U>>>18|O<<14)^(U<<23|O>>>9),Bt=h[T],wt=Bt.high,kt=Bt.low,St=N+gt,mt=j+yt+(St>>>0>>0?1:0),St=St+ut,mt=mt+ft+(St>>>0>>0?1:0),St=St+kt,mt=mt+wt+(St>>>0>>0?1:0),St=St+G,mt=mt+q+(St>>>0>>0?1:0),xt=_t+vt,bt=pt+dt+(xt>>>0<_t>>>0?1:0);j=X,N=L,X=I,L=K,I=O,K=U,U=W+St|0,O=P+mt+(U>>>0>>0?1:0)|0,P=M,W=F,M=R,F=E,R=C,E=D,D=St+xt|0,C=mt+bt+(D>>>0>>0?1:0)|0}v=i.low=v+D,i.high=d+C+(v>>>0>>0?1:0),_=n.low=_+E,n.high=p+R+(_>>>0>>0?1:0),g=o.low=g+F,o.high=y+M+(g>>>0>>0?1:0),w=s.low=w+W,s.high=B+P+(w>>>0>>0?1:0),S=a.low=S+U,a.high=k+O+(S>>>0>>0?1:0),x=c.low=x+K,c.high=m+I+(x>>>0>>0?1:0),H=f.low=H+L,f.high=b+X+(H>>>0>>0?1:0),A=u.low=A+N,u.high=z+j+(A>>>0>>0?1:0)},_doFinalize:function(){var t=this._data,r=t.words,e=8*this._nDataBytes,i=8*t.sigBytes;r[i>>>5]|=128<<24-i%32,r[(i+128>>>10<<5)+30]=Math.floor(e/4294967296),r[(i+128>>>10<<5)+31]=e,t.sigBytes=4*r.length,this._process();var n=this._hash.toX32();return n},clone:function(){var t=n.clone.call(this);return t._hash=this._hash.clone(),t},blockSize:32});e.SHA512=n._createHelper(f),e.HmacSHA512=n._createHmacHelper(f)}(),function(){var r=t,e=r.x64,i=e.Word,n=e.WordArray,o=r.algo,s=o.SHA512,a=o.SHA384=s.extend({_doReset:function(){this._hash=new n.init([new i.init(3418070365,3238371032),new i.init(1654270250,914150663),new i.init(2438529370,812702999),new i.init(355462360,4144912697),new i.init(1731405415,4290775857),new i.init(2394180231,1750603025),new i.init(3675008525,1694076839),new i.init(1203062813,3204075428)])},_doFinalize:function(){var t=s._doFinalize.call(this);return t.sigBytes-=16,t}});r.SHA384=s._createHelper(a),r.HmacSHA384=s._createHmacHelper(a)}(),t.lib.Cipher||function(r){var e=t,i=e.lib,n=i.Base,o=i.WordArray,s=i.BufferedBlockAlgorithm,a=e.enc,c=(a.Utf8,a.Base64),h=e.algo,l=h.EvpKDF,f=i.Cipher=s.extend({cfg:n.extend(),createEncryptor:function(t,r){return this.create(this._ENC_XFORM_MODE,t,r)},createDecryptor:function(t,r){return this.create(this._DEC_XFORM_MODE,t,r)},init:function(t,r,e){this.cfg=this.cfg.extend(e),this._xformMode=t,this._key=r,this.reset()},reset:function(){s.reset.call(this),this._doReset()},process:function(t){return this._append(t),this._process()},finalize:function(t){t&&this._append(t);var r=this._doFinalize();return r},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function t(t){return"string"==typeof t?m:w}return function(r){return{encrypt:function(e,i,n){return t(i).encrypt(r,e,i,n)},decrypt:function(e,i,n){return t(i).decrypt(r,e,i,n)}}}}()}),u=(i.StreamCipher=f.extend({_doFinalize:function(){var t=this._process(!0);return t},blockSize:1}),e.mode={}),d=i.BlockCipherMode=n.extend({createEncryptor:function(t,r){return this.Encryptor.create(t,r)},createDecryptor:function(t,r){return this.Decryptor.create(t,r)},init:function(t,r){this._cipher=t,this._iv=r}}),v=u.CBC=function(){function t(t,e,i){var n=this._iv;if(n){var o=n;this._iv=r}else var o=this._prevBlock;for(var s=0;s>>2];t.sigBytes-=r}},y=(i.BlockCipher=f.extend({cfg:f.cfg.extend({mode:v,padding:_}),reset:function(){f.reset.call(this);var t=this.cfg,r=t.iv,e=t.mode;if(this._xformMode==this._ENC_XFORM_MODE)var i=e.createEncryptor;else{var i=e.createDecryptor;this._minBufferSize=1}this._mode&&this._mode.__creator==i?this._mode.init(this,r&&r.words):(this._mode=i.call(e,this,r&&r.words),this._mode.__creator=i)},_doProcessBlock:function(t,r){this._mode.processBlock(t,r)},_doFinalize:function(){var t=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){t.pad(this._data,this.blockSize);var r=this._process(!0)}else{var r=this._process(!0);t.unpad(r)}return r},blockSize:4}),i.CipherParams=n.extend({init:function(t){this.mixIn(t)},toString:function(t){return(t||this.formatter).stringify(this)}})),g=e.format={},B=g.OpenSSL={stringify:function(t){var r=t.ciphertext,e=t.salt;if(e)var i=o.create([1398893684,1701076831]).concat(e).concat(r);else var i=r;return i.toString(c)},parse:function(t){var r=c.parse(t),e=r.words;if(1398893684==e[0]&&1701076831==e[1]){var i=o.create(e.slice(2,4));e.splice(0,4),r.sigBytes-=16}return y.create({ciphertext:r,salt:i})}},w=i.SerializableCipher=n.extend({cfg:n.extend({format:B}),encrypt:function(t,r,e,i){i=this.cfg.extend(i);var n=t.createEncryptor(e,i),o=n.finalize(r),s=n.cfg;return y.create({ciphertext:o,key:e,iv:s.iv,algorithm:t,mode:s.mode,padding:s.padding,blockSize:t.blockSize,formatter:i.format})},decrypt:function(t,r,e,i){i=this.cfg.extend(i),r=this._parse(r,i.format);var n=t.createDecryptor(e,i).finalize(r.ciphertext);return n},_parse:function(t,r){return"string"==typeof t?r.parse(t,this):t}}),k=e.kdf={},S=k.OpenSSL={execute:function(t,r,e,i){i||(i=o.random(8));var n=l.create({keySize:r+e}).compute(t,i),s=o.create(n.words.slice(r),4*e);return n.sigBytes=4*r,y.create({key:n,iv:s,salt:i})}},m=i.PasswordBasedCipher=w.extend({cfg:w.cfg.extend({kdf:S}),encrypt:function(t,r,e,i){i=this.cfg.extend(i);var n=i.kdf.execute(e,t.keySize,t.ivSize);i.iv=n.iv;var o=w.encrypt.call(this,t,r,n.key,i);return o.mixIn(n),o},decrypt:function(t,r,e,i){i=this.cfg.extend(i),r=this._parse(r,i.format);var n=i.kdf.execute(e,t.keySize,t.ivSize,r.salt);i.iv=n.iv;var o=w.decrypt.call(this,t,r,n.key,i);return o}})}(),t.mode.CFB=function(){function r(t,r,e,i){var n=this._iv;if(n){var o=n.slice(0);this._iv=void 0}else var o=this._prevBlock;i.encryptBlock(o,0);for(var s=0;s>>2]|=n<<24-o%4*8,t.sigBytes+=n},unpad:function(t){var r=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=r}},t.pad.Iso10126={pad:function(r,e){var i=4*e,n=i-r.sigBytes%i;r.concat(t.lib.WordArray.random(n-1)).concat(t.lib.WordArray.create([n<<24],1))},unpad:function(t){var r=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=r}},t.pad.Iso97971={pad:function(r,e){r.concat(t.lib.WordArray.create([2147483648],1)),t.pad.ZeroPadding.pad(r,e)},unpad:function(r){t.pad.ZeroPadding.unpad(r),r.sigBytes--}},t.mode.OFB=function(){var r=t.lib.BlockCipherMode.extend(),e=r.Encryptor=r.extend({processBlock:function(t,r){var e=this._cipher,i=e.blockSize,n=this._iv,o=this._keystream;n&&(o=this._keystream=n.slice(0),this._iv=void 0),e.encryptBlock(o,0);for(var s=0;s>>8^255&n^99,o[e]=n,s[n]=e;var p=t[e],_=t[p],y=t[_],g=257*t[n]^16843008*n;a[e]=g<<24|g>>>8,c[e]=g<<16|g>>>16,h[e]=g<<8|g>>>24,l[e]=g;var g=16843009*y^65537*_^257*p^16843008*e;f[n]=g<<24|g>>>8,u[n]=g<<16|g>>>16,d[n]=g<<8|g>>>24,v[n]=g,e?(e=p^t[t[t[y^p]]],i^=t[t[i]]):e=i=1}}();var p=[0,1,2,4,8,16,32,64,128,27,54],_=n.AES=i.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var t=this._keyPriorReset=this._key,r=t.words,e=t.sigBytes/4,i=this._nRounds=e+6,n=4*(i+1),s=this._keySchedule=[],a=0;a6&&a%e==4&&(c=o[c>>>24]<<24|o[c>>>16&255]<<16|o[c>>>8&255]<<8|o[255&c]):(c=c<<8|c>>>24,c=o[c>>>24]<<24|o[c>>>16&255]<<16|o[c>>>8&255]<<8|o[255&c],c^=p[a/e|0]<<24),s[a]=s[a-e]^c}for(var h=this._invKeySchedule=[],l=0;l>>24]]^u[o[c>>>16&255]]^d[o[c>>>8&255]]^v[o[255&c]]}}},encryptBlock:function(t,r){this._doCryptBlock(t,r,this._keySchedule,a,c,h,l,o)},decryptBlock:function(t,r){var e=t[r+1];t[r+1]=t[r+3],t[r+3]=e,this._doCryptBlock(t,r,this._invKeySchedule,f,u,d,v,s);var e=t[r+1];t[r+1]=t[r+3],t[r+3]=e},_doCryptBlock:function(t,r,e,i,n,o,s,a){for(var c=this._nRounds,h=t[r]^e[0],l=t[r+1]^e[1],f=t[r+2]^e[2],u=t[r+3]^e[3],d=4,v=1;v>>24]^n[l>>>16&255]^o[f>>>8&255]^s[255&u]^e[d++],_=i[l>>>24]^n[f>>>16&255]^o[u>>>8&255]^s[255&h]^e[d++],y=i[f>>>24]^n[u>>>16&255]^o[h>>>8&255]^s[255&l]^e[d++],g=i[u>>>24]^n[h>>>16&255]^o[l>>>8&255]^s[255&f]^e[d++];h=p,l=_,f=y,u=g}var p=(a[h>>>24]<<24|a[l>>>16&255]<<16|a[f>>>8&255]<<8|a[255&u])^e[d++],_=(a[l>>>24]<<24|a[f>>>16&255]<<16|a[u>>>8&255]<<8|a[255&h])^e[d++],y=(a[f>>>24]<<24|a[u>>>16&255]<<16|a[h>>>8&255]<<8|a[255&l])^e[d++],g=(a[u>>>24]<<24|a[h>>>16&255]<<16|a[l>>>8&255]<<8|a[255&f])^e[d++];t[r]=p,t[r+1]=_,t[r+2]=y,t[r+3]=g},keySize:8});r.AES=i._createHelper(_)}(),function(){function r(t,r){var e=(this._lBlock>>>t^this._rBlock)&r;this._rBlock^=e,this._lBlock^=e<>>t^this._lBlock)&r;this._lBlock^=e,this._rBlock^=e<>>5]>>>31-n%32&1}for(var o=this._subKeys=[],s=0;s<16;s++){for(var a=o[s]=[],f=l[s],i=0;i<24;i++)a[i/6|0]|=e[(h[i]-1+f)%28]<<31-i%6,a[4+(i/6|0)]|=e[28+(h[i+24]-1+f)%28]<<31-i%6;a[0]=a[0]<<1|a[0]>>>31;for(var i=1;i<7;i++)a[i]=a[i]>>>4*(i-1)+3;a[7]=a[7]<<5|a[7]>>>27}for(var u=this._invSubKeys=[],i=0;i<16;i++)u[i]=o[15-i]},encryptBlock:function(t,r){this._doCryptBlock(t,r,this._subKeys)},decryptBlock:function(t,r){this._doCryptBlock(t,r,this._invSubKeys)},_doCryptBlock:function(t,i,n){this._lBlock=t[i],this._rBlock=t[i+1],r.call(this,4,252645135),r.call(this,16,65535),e.call(this,2,858993459),e.call(this,8,16711935),r.call(this,1,1431655765);for(var o=0;o<16;o++){for(var s=n[o],a=this._lBlock,c=this._rBlock,h=0,l=0;l<8;l++)h|=f[l][((c^s[l])&u[l])>>>0];this._lBlock=c,this._rBlock=a^h}var d=this._lBlock;this._lBlock=this._rBlock,this._rBlock=d,r.call(this,1,1431655765),e.call(this,8,16711935),e.call(this,2,858993459),r.call(this,16,65535),r.call(this,4,252645135),t[i]=this._lBlock,t[i+1]=this._rBlock},keySize:2,ivSize:2,blockSize:2});i.DES=s._createHelper(d);var v=a.TripleDES=s.extend({_doReset:function(){var t=this._key,r=t.words;this._des1=d.createEncryptor(o.create(r.slice(0,2))),this._des2=d.createEncryptor(o.create(r.slice(2,4))),this._des3=d.createEncryptor(o.create(r.slice(4,6)))},encryptBlock:function(t,r){this._des1.encryptBlock(t,r),this._des2.decryptBlock(t,r),this._des3.encryptBlock(t,r)},decryptBlock:function(t,r){this._des3.decryptBlock(t,r),this._des2.encryptBlock(t,r),this._des1.decryptBlock(t,r)},keySize:6,ivSize:2,blockSize:2});i.TripleDES=s._createHelper(v)}(),function(){function r(){for(var t=this._S,r=this._i,e=this._j,i=0,n=0;n<4;n++){r=(r+1)%256,e=(e+t[r])%256;var o=t[r];t[r]=t[e],t[e]=o,i|=t[(t[r]+t[e])%256]<<24-8*n}return this._i=r,this._j=e,i}var e=t,i=e.lib,n=i.StreamCipher,o=e.algo,s=o.RC4=n.extend({_doReset:function(){for(var t=this._key,r=t.words,e=t.sigBytes,i=this._S=[],n=0;n<256;n++)i[n]=n;for(var n=0,o=0;n<256;n++){var s=n%e,a=r[s>>>2]>>>24-s%4*8&255;o=(o+i[n]+a)%256;var c=i[n];i[n]=i[o],i[o]=c}this._i=this._j=0},_doProcessBlock:function(t,e){t[e]^=r.call(this)},keySize:8,ivSize:0});e.RC4=n._createHelper(s);var a=o.RC4Drop=s.extend({cfg:s.cfg.extend({drop:192}),_doReset:function(){s._doReset.call(this);for(var t=this.cfg.drop;t>0;t--)r.call(this)}});e.RC4Drop=n._createHelper(a)}(),t.mode.CTRGladman=function(){function r(t){if(255===(t>>24&255)){var r=t>>16&255,e=t>>8&255,i=255&t;255===r?(r=0,255===e?(e=0,255===i?i=0:++i):++e):++r,t=0,t+=r<<16,t+=e<<8,t+=i}else t+=1<<24;return t}function e(t){return 0===(t[0]=r(t[0]))&&(t[1]=r(t[1])),t}var i=t.lib.BlockCipherMode.extend(),n=i.Encryptor=i.extend({processBlock:function(t,r){var i=this._cipher,n=i.blockSize,o=this._iv,s=this._counter;o&&(s=this._counter=o.slice(0),this._iv=void 0),e(s);var a=s.slice(0);i.encryptBlock(a,0);for(var c=0;c>>0>>0?1:0)|0,r[2]=r[2]+886263092+(r[1]>>>0>>0?1:0)|0,r[3]=r[3]+1295307597+(r[2]>>>0>>0?1:0)|0,r[4]=r[4]+3545052371+(r[3]>>>0>>0?1:0)|0,r[5]=r[5]+886263092+(r[4]>>>0>>0?1:0)|0,r[6]=r[6]+1295307597+(r[5]>>>0>>0?1:0)|0,r[7]=r[7]+3545052371+(r[6]>>>0>>0?1:0)|0,this._b=r[7]>>>0>>0?1:0;for(var e=0;e<8;e++){var i=t[e]+r[e],n=65535&i,o=i>>>16,s=((n*n>>>17)+n*o>>>15)+o*o,h=((4294901760&i)*i|0)+((65535&i)*i|0);c[e]=s^h}t[0]=c[0]+(c[7]<<16|c[7]>>>16)+(c[6]<<16|c[6]>>>16)|0,t[1]=c[1]+(c[0]<<8|c[0]>>>24)+c[7]|0,t[2]=c[2]+(c[1]<<16|c[1]>>>16)+(c[0]<<16|c[0]>>>16)|0,t[3]=c[3]+(c[2]<<8|c[2]>>>24)+c[1]|0,t[4]=c[4]+(c[3]<<16|c[3]>>>16)+(c[2]<<16|c[2]>>>16)|0,t[5]=c[5]+(c[4]<<8|c[4]>>>24)+c[3]|0,t[6]=c[6]+(c[5]<<16|c[5]>>>16)+(c[4]<<16|c[4]>>>16)|0,t[7]=c[7]+(c[6]<<8|c[6]>>>24)+c[5]|0}var e=t,i=e.lib,n=i.StreamCipher,o=e.algo,s=[],a=[],c=[],h=o.Rabbit=n.extend({_doReset:function(){for(var t=this._key.words,e=this.cfg.iv,i=0;i<4;i++)t[i]=16711935&(t[i]<<8|t[i]>>>24)|4278255360&(t[i]<<24|t[i]>>>8);var n=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],o=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]];this._b=0;for(var i=0;i<4;i++)r.call(this);for(var i=0;i<8;i++)o[i]^=n[i+4&7];if(e){var s=e.words,a=s[0],c=s[1],h=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),l=16711935&(c<<8|c>>>24)|4278255360&(c<<24|c>>>8),f=h>>>16|4294901760&l,u=l<<16|65535&h;o[0]^=h,o[1]^=f,o[2]^=l,o[3]^=u,o[4]^=h,o[5]^=f,o[6]^=l,o[7]^=u;for(var i=0;i<4;i++)r.call(this)}},_doProcessBlock:function(t,e){var i=this._X;r.call(this),s[0]=i[0]^i[5]>>>16^i[3]<<16,s[1]=i[2]^i[7]>>>16^i[5]<<16,s[2]=i[4]^i[1]>>>16^i[7]<<16,s[3]=i[6]^i[3]>>>16^i[1]<<16;for(var n=0;n<4;n++)s[n]=16711935&(s[n]<<8|s[n]>>>24)|4278255360&(s[n]<<24|s[n]>>>8),t[e+n]^=s[n]},blockSize:4,ivSize:2});e.Rabbit=n._createHelper(h)}(),t.mode.CTR=function(){var r=t.lib.BlockCipherMode.extend(),e=r.Encryptor=r.extend({processBlock:function(t,r){var e=this._cipher,i=e.blockSize,n=this._iv,o=this._counter;n&&(o=this._counter=n.slice(0),this._iv=void 0);var s=o.slice(0);e.encryptBlock(s,0),o[i-1]=o[i-1]+1|0;for(var a=0;a>>0>>0?1:0)|0,r[2]=r[2]+886263092+(r[1]>>>0>>0?1:0)|0,r[3]=r[3]+1295307597+(r[2]>>>0>>0?1:0)|0,r[4]=r[4]+3545052371+(r[3]>>>0>>0?1:0)|0,r[5]=r[5]+886263092+(r[4]>>>0>>0?1:0)|0,r[6]=r[6]+1295307597+(r[5]>>>0>>0?1:0)|0,r[7]=r[7]+3545052371+(r[6]>>>0>>0?1:0)|0,this._b=r[7]>>>0>>0?1:0;for(var e=0;e<8;e++){var i=t[e]+r[e],n=65535&i,o=i>>>16,s=((n*n>>>17)+n*o>>>15)+o*o,h=((4294901760&i)*i|0)+((65535&i)*i|0);c[e]=s^h}t[0]=c[0]+(c[7]<<16|c[7]>>>16)+(c[6]<<16|c[6]>>>16)|0,t[1]=c[1]+(c[0]<<8|c[0]>>>24)+c[7]|0,t[2]=c[2]+(c[1]<<16|c[1]>>>16)+(c[0]<<16|c[0]>>>16)|0,t[3]=c[3]+(c[2]<<8|c[2]>>>24)+c[1]|0,t[4]=c[4]+(c[3]<<16|c[3]>>>16)+(c[2]<<16|c[2]>>>16)|0,t[5]=c[5]+(c[4]<<8|c[4]>>>24)+c[3]|0,t[6]=c[6]+(c[5]<<16|c[5]>>>16)+(c[4]<<16|c[4]>>>16)|0,t[7]=c[7]+(c[6]<<8|c[6]>>>24)+c[5]|0}var e=t,i=e.lib,n=i.StreamCipher,o=e.algo,s=[],a=[],c=[],h=o.RabbitLegacy=n.extend({_doReset:function(){var t=this._key.words,e=this.cfg.iv,i=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],n=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]];this._b=0;for(var o=0;o<4;o++)r.call(this);for(var o=0;o<8;o++)n[o]^=i[o+4&7];if(e){var s=e.words,a=s[0],c=s[1],h=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),l=16711935&(c<<8|c>>>24)|4278255360&(c<<24|c>>>8),f=h>>>16|4294901760&l,u=l<<16|65535&h;n[0]^=h,n[1]^=f,n[2]^=l,n[3]^=u,n[4]^=h,n[5]^=f,n[6]^=l,n[7]^=u;for(var o=0;o<4;o++)r.call(this)}},_doProcessBlock:function(t,e){var i=this._X;r.call(this),s[0]=i[0]^i[5]>>>16^i[3]<<16,s[1]=i[2]^i[7]>>>16^i[5]<<16,s[2]=i[4]^i[1]>>>16^i[7]<<16,s[3]=i[6]^i[3]>>>16^i[1]<<16;for(var n=0;n<4;n++)s[n]=16711935&(s[n]<<8|s[n]>>>24)|4278255360&(s[n]<<24|s[n]>>>8),t[e+n]^=s[n]},blockSize:4,ivSize:2});e.RabbitLegacy=n._createHelper(h)}(),t.pad.ZeroPadding={pad:function(t,r){var e=4*r;t.clamp(),t.sigBytes+=e-(t.sigBytes%e||e)},unpad:function(t){for(var r=t.words,e=t.sigBytes-1;!(r[e>>>2]>>>24-e%4*8&255);)e--;t.sigBytes=e+1}},t}); +//# sourceMappingURL=crypto-js.min.js.map \ No newline at end of file diff --git a/frontend-web/vendor/js/fabric.min.js b/frontend-web/vendor/js/fabric.min.js new file mode 100644 index 0000000..790401c --- /dev/null +++ b/frontend-web/vendor/js/fabric.min.js @@ -0,0 +1,30786 @@ +/* build: `node build.js modules=ALL exclude=gestures,accessors,erasing requirejs minifier=uglifyjs` */ +/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: '4.6.0' }; +if (typeof exports !== 'undefined') { + exports.fabric = fabric; +} +/* _AMD_START_ */ +else if (typeof define === 'function' && define.amd) { + define([], function() { return fabric; }); +} +/* _AMD_END_ */ +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + if (document instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document)) { + fabric.document = document; + } + else { + fabric.document = document.implementation.createHTMLDocument(''); + } + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + var jsdom = require('jsdom'); + var virtualWindow = new jsdom.JSDOM( + decodeURIComponent('%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E'), + { + features: { + FetchExternalResources: ['img'] + }, + resources: 'usable' + }).window; + fabric.document = virtualWindow.document; + fabric.jsdomImplForWrapper = require('jsdom/lib/jsdom/living/generated/utils').implForWrapper; + fabric.nodeCanvas = require('jsdom/lib/jsdom/utils').Canvas; + fabric.window = virtualWindow; + DOMParser = fabric.window.DOMParser; +} + +/** + * True when in environment that supports touch events + * @type boolean + */ +fabric.isTouchSupported = 'ontouchstart' in fabric.window || 'ontouchstart' in fabric.document || + (fabric.window && fabric.window.navigator && fabric.window.navigator.maxTouchPoints > 0); + +/** + * True when in environment that's probably Node.js + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; + +/* _FROM_SVG_START_ */ +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + 'display', + 'transform', + 'fill', 'fill-opacity', 'fill-rule', + 'opacity', + 'stroke', 'stroke-dasharray', 'stroke-linecap', 'stroke-dashoffset', + 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-opacity', 'stroke-width', + 'id', 'paint-order', 'vector-effect', + 'instantiated_by_use', 'clip-path', +]; +/* _FROM_SVG_END_ */ + +/** + * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. + */ +fabric.DPI = 96; +fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)'; +fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'; +fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig; +fabric.reNonWord = /[ \n\.,;!\?\-]/; +fabric.fontPaths = { }; +fabric.iMatrix = [1, 0, 0, 1, 0, 0]; +fabric.svgNS = 'http://www.w3.org/2000/svg'; + +/** + * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine. + * @since 1.7.14 + * @type Number + * @default + */ +fabric.perfLimitSizeTotal = 2097152; + +/** + * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000 + * @since 1.7.14 + * @type Number + * @default + */ +fabric.maxCacheSideLimit = 4096; + +/** + * Lowest pixel limit for cache canvases, set at 256PX + * @since 1.7.14 + * @type Number + * @default + */ +fabric.minCacheSideLimit = 256; + +/** + * Cache Object for widths of chars in text rendering. + */ +fabric.charWidthsCache = { }; + +/** + * if webgl is enabled and available, textureSize will determine the size + * of the canvas backend + * @since 2.0.0 + * @type Number + * @default + */ +fabric.textureSize = 2048; + +/** + * When 'true', style information is not retained when copy/pasting text, making + * pasted text use destination style. + * Defaults to 'false'. + * @type Boolean + * @default + */ +fabric.disableStyleCopyPaste = false; + +/** + * Enable webgl for filtering picture is available + * A filtering backend will be initialized, this will both take memory and + * time since a default 2048x2048 canvas will be created for the gl context + * @since 2.0.0 + * @type Boolean + * @default + */ +fabric.enableGLFiltering = true; + +/** + * Device Pixel Ratio + * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html + */ +fabric.devicePixelRatio = fabric.window.devicePixelRatio || + fabric.window.webkitDevicePixelRatio || + fabric.window.mozDevicePixelRatio || + 1; +/** + * Browser-specific constant to adjust CanvasRenderingContext2D.shadowBlur value, + * which is unitless and not rendered equally across browsers. + * + * Values that work quite well (as of October 2017) are: + * - Chrome: 1.5 + * - Edge: 1.75 + * - Firefox: 0.9 + * - Safari: 0.95 + * + * @since 2.0.0 + * @type Number + * @default 1 + */ +fabric.browserShadowBlurConstant = 1; + +/** + * This object contains the result of arc to bezier conversion for faster retrieving if the same arc needs to be converted again. + * It was an internal variable, is accessible since version 2.3.4 + */ +fabric.arcToSegmentsCache = { }; + +/** + * This object keeps the results of the boundsOfCurve calculation mapped by the joined arguments necessary to calculate it. + * It does speed up calculation, if you parse and add always the same paths, but in case of heavy usage of freedrawing + * you do not get any speed benefit and you get a big object in memory. + * The object was a private variable before, while now is appended to the lib so that you have access to it and you + * can eventually clear it. + * It was an internal variable, is accessible since version 2.3.4 + */ +fabric.boundsOfCurveCache = { }; + +/** + * If disabled boundsOfCurveCache is not used. For apps that make heavy usage of pencil drawing probably disabling it is better + * @default true + */ +fabric.cachesBoundsOfCurve = true; + +/** + * Skip performance testing of setupGLContext and force the use of putImageData that seems to be the one that works best on + * Chrome + old hardware. if your users are experiencing empty images after filtering you may try to force this to true + * this has to be set before instantiating the filtering backend ( before filtering the first image ) + * @type Boolean + * @default false + */ +fabric.forceGLPutImageData = false; + +fabric.initFilterBackend = function() { + if (fabric.enableGLFiltering && fabric.isWebglSupported && fabric.isWebglSupported(fabric.textureSize)) { + console.log('max texture size: ' + fabric.maxTextureSize); + return (new fabric.WebglFilterBackend({ tileSize: fabric.textureSize })); + } + else if (fabric.Canvas2dFilterBackend) { + return (new fabric.Canvas2dFilterBackend()); + } +}; + + +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) + window.fabric = fabric; +} + + +(function() { + + /** + * @private + * @param {String} eventName + * @param {Function} handler + */ + function _removeEventListener(eventName, handler) { + if (!this.__eventListeners[eventName]) { + return; + } + var eventListener = this.__eventListeners[eventName]; + if (handler) { + eventListener[eventListener.indexOf(handler)] = false; + } + else { + fabric.util.array.fill(eventListener, false); + } + } + + /** + * Observes specified event + * @memberOf fabric.Observable + * @alias on + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function that receives a notification when an event of the specified type occurs + * @return {Self} thisArg + * @chainable + */ + function on(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = []; + } + this.__eventListeners[eventName].push(handler); + } + return this; + } + + function _once(eventName, handler) { + var _handler = function () { + handler.apply(this, arguments); + this.off(eventName, _handler); + }.bind(this); + this.on(eventName, _handler); + } + + function once(eventName, handler) { + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + _once.call(this, prop, eventName[prop]); + } + } + else { + _once.call(this, eventName, handler); + } + return this; + } + + /** + * Stops event observing for a particular event handler. Calling this method + * without arguments removes all handlers for all events + * @memberOf fabric.Observable + * @alias off + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function to be deleted from EventListeners + * @return {Self} thisArg + * @chainable + */ + function off(eventName, handler) { + if (!this.__eventListeners) { + return this; + } + + // remove all key/value pairs (event name -> event handler) + if (arguments.length === 0) { + for (eventName in this.__eventListeners) { + _removeEventListener.call(this, eventName); + } + } + // one object with key/value pairs was passed + else if (arguments.length === 1 && typeof arguments[0] === 'object') { + for (var prop in eventName) { + _removeEventListener.call(this, prop, eventName[prop]); + } + } + else { + _removeEventListener.call(this, eventName, handler); + } + return this; + } + + /** + * Fires event with an optional options object + * @memberOf fabric.Observable + * @param {String} eventName Event name to fire + * @param {Object} [options] Options object + * @return {Self} thisArg + * @chainable + */ + function fire(eventName, options) { + if (!this.__eventListeners) { + return this; + } + + var listenersForEvent = this.__eventListeners[eventName]; + if (!listenersForEvent) { + return this; + } + + for (var i = 0, len = listenersForEvent.length; i < len; i++) { + listenersForEvent[i] && listenersForEvent[i].call(this, options || { }); + } + this.__eventListeners[eventName] = listenersForEvent.filter(function(value) { + return value !== false; + }); + return this; + } + + /** + * @namespace fabric.Observable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events} + * @see {@link http://fabricjs.com/events|Events demo} + */ + fabric.Observable = { + fire: fire, + on: on, + once: once, + off: off, + }; +})(); + + +/** + * @namespace fabric.Collection + */ +fabric.Collection = { + + _objects: [], + + /** + * Adds objects to collection, Canvas or Group, then renders canvas + * (if `renderOnAddRemove` is not `false`). + * in case of Group no changes to bounding box are made. + * Objects should be instances of (or inherit from) fabric.Object + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the add method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + if (this._onObjectAdded) { + for (var i = 0, length = arguments.length; i < length; i++) { + this._onObjectAdded(arguments[i]); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) + * An object should be an instance of (or inherit from) fabric.Object + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the insertAt method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {Object} object Object to insert + * @param {Number} index Index to insert object at + * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs + * @return {Self} thisArg + * @chainable + */ + insertAt: function (object, index, nonSplicing) { + var objects = this._objects; + if (nonSplicing) { + objects[index] = object; + } + else { + objects.splice(index, 0, object); + } + this._onObjectAdded && this._onObjectAdded(object); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + remove: function() { + var objects = this._objects, + index, somethingRemoved = false; + + for (var i = 0, length = arguments.length; i < length; i++) { + index = objects.indexOf(arguments[i]); + + // only call onObjectRemoved if an object was actually removed + if (index !== -1) { + somethingRemoved = true; + objects.splice(index, 1); + this._onObjectRemoved && this._onObjectRemoved(arguments[i]); + } + } + + this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); + return this; + }, + + /** + * Executes given function for each object in this group + * @param {Function} callback + * Callback invoked with current object as first argument, + * index - as second and an array of all objects - as third. + * Callback is invoked in a context of Global Object (e.g. `window`) + * when no `context` argument is given + * + * @param {Object} context Context (aka thisObject) + * @return {Self} thisArg + * @chainable + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(); + for (var i = 0, len = objects.length; i < len; i++) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Returns an array of children objects of this instance + * Type parameter introduced in 1.3.10 + * since 2.3.5 this method return always a COPY of the array; + * @param {String} [type] When specified, only objects of this type are returned + * @return {Array} + */ + getObjects: function(type) { + if (typeof type === 'undefined') { + return this._objects.concat(); + } + return this._objects.filter(function(o) { + return o.type === type; + }); + }, + + /** + * Returns object at specified index + * @param {Number} index + * @return {Self} thisArg + */ + item: function (index) { + return this._objects[index]; + }, + + /** + * Returns true if collection contains no objects + * @return {Boolean} true if collection is empty + */ + isEmpty: function () { + return this._objects.length === 0; + }, + + /** + * Returns a size of a collection (i.e: length of an array containing its objects) + * @return {Number} Collection size + */ + size: function() { + return this._objects.length; + }, + + /** + * Returns true if collection contains an object + * @param {Object} object Object to check against + * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` + * @return {Boolean} `true` if collection contains an object + */ + contains: function (object, deep) { + if (this._objects.indexOf(object) > -1) { + return true; + } + else if (deep) { + return this._objects.some(function (obj) { + return typeof obj.contains === 'function' && obj.contains(object, true); + }); + } + return false; + }, + + /** + * Returns number representation of a collection complexity + * @return {Number} complexity + */ + complexity: function () { + return this._objects.reduce(function (memo, current) { + memo += current.complexity ? current.complexity() : 0; + return memo; + }, 0); + } +}; + + +/** + * @namespace fabric.CommonMethods + */ +fabric.CommonMethods = { + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + _setOptions: function(options) { + for (var prop in options) { + this.set(prop, options[prop]); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Gradient to + */ + _initGradient: function(filler, property) { + if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { + this.set(property, new fabric.Gradient(filler)); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Pattern to + * @param {Function} [callback] callback to invoke after pattern load + */ + _initPattern: function(filler, property, callback) { + if (filler && filler.source && !(filler instanceof fabric.Pattern)) { + this.set(property, new fabric.Pattern(filler, callback)); + } + else { + callback && callback(); + } + }, + + /** + * @private + */ + _setObject: function(obj) { + for (var prop in obj) { + this._set(prop, obj[prop]); + } + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + this._setObject(key); + } + else { + this._set(key, value); + } + return this; + }, + + _set: function(key, value) { + this[key] = value; + }, + + /** + * Toggles specified property from `true` to `false` or from `false` to `true` + * @param {String} property Property to toggle + * @return {fabric.Object} thisArg + * @chainable + */ + toggle: function(property) { + var value = this.get(property); + if (typeof value === 'boolean') { + this.set(property, !value); + } + return this; + }, + + /** + * Basic getter + * @param {String} property Property name + * @return {*} value of a property + */ + get: function(property) { + return this[property]; + } +}; + + +(function(global) { + + var sqrt = Math.sqrt, + atan2 = Math.atan2, + pow = Math.pow, + PiBy180 = Math.PI / 180, + PiBy2 = Math.PI / 2; + + /** + * @namespace fabric.util + */ + fabric.util = { + + /** + * Calculate the cos of an angle, avoiding returning floats for known results + * @static + * @memberOf fabric.util + * @param {Number} angle the angle in radians or in degree + * @return {Number} + */ + cos: function(angle) { + if (angle === 0) { return 1; } + if (angle < 0) { + // cos(a) = cos(-a) + angle = -angle; + } + var angleSlice = angle / PiBy2; + switch (angleSlice) { + case 1: case 3: return 0; + case 2: return -1; + } + return Math.cos(angle); + }, + + /** + * Calculate the sin of an angle, avoiding returning floats for known results + * @static + * @memberOf fabric.util + * @param {Number} angle the angle in radians or in degree + * @return {Number} + */ + sin: function(angle) { + if (angle === 0) { return 0; } + var angleSlice = angle / PiBy2, sign = 1; + if (angle < 0) { + // sin(-a) = -sin(a) + sign = -1; + } + switch (angleSlice) { + case 1: return sign; + case 2: return 0; + case 3: return -sign; + } + return Math.sin(angle); + }, + + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @param {Array} array + * @param {*} value + * @return {Array} original array + */ + removeFromArray: function(array, value) { + var idx = array.indexOf(value); + if (idx !== -1) { + array.splice(idx, 1); + } + return array; + }, + + /** + * Returns random number between 2 specified ones. + * @static + * @memberOf fabric.util + * @param {Number} min lower limit + * @param {Number} max upper limit + * @return {Number} random value (between min and max) + */ + getRandomInt: function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, + + /** + * Transforms degrees to radians. + * @static + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + degreesToRadians: function(degrees) { + return degrees * PiBy180; + }, + + /** + * Transforms radians to degrees. + * @static + * @memberOf fabric.util + * @param {Number} radians value in radians + * @return {Number} value in degrees + */ + radiansToDegrees: function(radians) { + return radians / PiBy180; + }, + + /** + * Rotates `point` around `origin` with `radians` + * @static + * @memberOf fabric.util + * @param {fabric.Point} point The point to rotate + * @param {fabric.Point} origin The origin of the rotation + * @param {Number} radians The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), + v = fabric.util.rotateVector(newPoint, radians); + return new fabric.Point(v.x, v.y).addEquals(origin); + }, + + /** + * Rotates `vector` with `radians` + * @static + * @memberOf fabric.util + * @param {Object} vector The vector to rotate (x and y) + * @param {Number} radians The radians of the angle for the rotation + * @return {Object} The new rotated point + */ + rotateVector: function(vector, radians) { + var sin = fabric.util.sin(radians), + cos = fabric.util.cos(radians), + rx = vector.x * cos - vector.y * sin, + ry = vector.x * sin + vector.y * cos; + return { + x: rx, + y: ry + }; + }, + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[2] * p.y, + t[1] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[2] * p.y + t[4], + t[1] * p.x + t[3] * p.y + t[5] + ); + }, + + /** + * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @param {Array} points 4 points array + * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix + * @return {Object} Object with left, top, width, height properties + */ + makeBoundingBoxFromPoints: function(points, transform) { + if (transform) { + for (var i = 0; i < points.length; i++) { + points[i] = fabric.util.transformPoint(points[i], transform); + } + } + var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], + minX = fabric.util.array.min(xPoints), + maxX = fabric.util.array.max(xPoints), + width = maxX - minX, + yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], + minY = fabric.util.array.min(yPoints), + maxY = fabric.util.array.max(yPoints), + height = maxY - minY; + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var a = 1 / (t[0] * t[3] - t[1] * t[2]), + r = [a * t[3], -a * t[1], -a * t[2], a * t[0]], + o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true); + r[4] = -o.x; + r[5] = -o.y; + return r; + }, + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @memberOf fabric.util + * @param {Number|String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, + + /** + * Converts from attribute value to pixel value if applicable. + * Returns converted pixels or original value not converted. + * @param {Number|String} value number to operate on + * @param {Number} fontSize + * @return {Number|String} + */ + parseUnit: function(value, fontSize) { + var unit = /\D{0,2}$/.exec(value), + number = parseFloat(value); + if (!fontSize) { + fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + switch (unit[0]) { + case 'mm': + return number * fabric.DPI / 25.4; + + case 'cm': + return number * fabric.DPI / 2.54; + + case 'in': + return number * fabric.DPI; + + case 'pt': + return number * fabric.DPI / 72; // or * 4 / 3 + + case 'pc': + return number * fabric.DPI / 72 * 12; // or * 16 + + case 'em': + return number * fontSize; + + default: + return number; + } + }, + + /** + * Function which always returns `false`. + * @static + * @memberOf fabric.util + * @return {Boolean} + */ + falseFunction: function() { + return false; + }, + + /** + * Returns klass "Class" object of given namespace + * @memberOf fabric.util + * @param {String} type Type of object (eg. 'circle') + * @param {String} namespace Namespace to get klass "Class" object from + * @return {Object} klass "Class" + */ + getKlass: function(type, namespace) { + // capitalize first letter only + type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); + return fabric.util.resolveNamespace(namespace)[type]; + }, + + /** + * Returns array of attributes for given svg that fabric parses + * @memberOf fabric.util + * @param {String} type Type of svg element (eg. 'circle') + * @return {Array} string names of supported attributes + */ + getSvgAttributes: function(type) { + var attributes = [ + 'instantiated_by_use', + 'style', + 'id', + 'class' + ]; + switch (type) { + case 'linearGradient': + attributes = attributes.concat(['x1', 'y1', 'x2', 'y2', 'gradientUnits', 'gradientTransform']); + break; + case 'radialGradient': + attributes = attributes.concat(['gradientUnits', 'gradientTransform', 'cx', 'cy', 'r', 'fx', 'fy', 'fr']); + break; + case 'stop': + attributes = attributes.concat(['offset', 'stop-color', 'stop-opacity']); + break; + } + return attributes; + }, + + /** + * Returns object of given namespace + * @memberOf fabric.util + * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' + * @return {Object} Object for given namespace (default fabric) + */ + resolveNamespace: function(namespace) { + if (!namespace) { + return fabric; + } + + var parts = namespace.split('.'), + len = parts.length, i, + obj = global || fabric.window; + + for (i = 0; i < len; ++i) { + obj = obj[parts[i]]; + } + + return obj; + }, + + /** + * Loads image element from given url and passes it to a callback + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {*} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } + + var img = fabric.util.createImage(); + + /** @ignore */ + var onLoadCallback = function () { + callback && callback.call(context, img, false); + img = img.onload = img.onerror = null; + }; + + img.onload = onLoadCallback; + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + // crossOrigin null is the same as not set. + if (url.indexOf('data') !== 0 && + crossOrigin !== undefined && + crossOrigin !== null) { + img.crossOrigin = crossOrigin; + } + + // IE10 / IE11-Fix: SVG contents from data: URI + // will only be available if the IMG is present + // in the DOM (and visible) + if (url.substring(0,14) === 'data:image/svg') { + img.onload = null; + fabric.util.loadImageInDom(img, onLoadCallback); + } + + img.src = url; + }, + + /** + * Attaches SVG image with data: URL to the dom + * @memberOf fabric.util + * @param {Object} img Image object with data:image/svg src + * @param {Function} callback Callback; invoked with loaded image + * @return {Object} DOM element (div containing the SVG image) + */ + loadImageInDom: function(img, onLoadCallback) { + var div = fabric.document.createElement('div'); + div.style.width = div.style.height = '1px'; + div.style.left = div.style.top = '-100%'; + div.style.position = 'absolute'; + div.appendChild(img); + fabric.document.querySelector('body').appendChild(div); + /** + * Wrap in function to: + * 1. Call existing callback + * 2. Cleanup DOM + */ + img.onload = function () { + onLoadCallback(); + div.parentNode.removeChild(div); + div = null; + }; + }, + + /** + * Creates corresponding fabric instances from their object representations + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {String} namespace Namespace to get klass "Class" object from + * @param {Function} reviver Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || []; + + var enlivenedObjects = [], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects.filter(function(obj) { + // filter out undefined objects (objects that gave error) + return obj; + })); + } + } + + if (!numTotalObjects) { + callback && callback(enlivenedObjects); + return; + } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { + onLoaded(); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + klass.fromObject(o, function (obj, error) { + error || (enlivenedObjects[index] = obj); + reviver && reviver(o, obj, error); + onLoaded(); + }); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} patterns Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * called after each fabric object created. + */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; + + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); + } + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; + onLoaded(); + }); + } + else { + enlivenedPatterns[index] = p; + onLoaded(); + } + }); + }, + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @param {String} path Value to set sourcePath to + * @return {fabric.Object|fabric.Group} + */ + groupSVGElements: function(elements, options, path) { + var object; + if (elements && elements.length === 1) { + return elements[0]; + } + if (options) { + if (options.width && options.height) { + options.centerPoint = { + x: options.width / 2, + y: options.height / 2 + }; + } + else { + delete options.width; + delete options.height; + } + } + object = new fabric.Group(elements, options); + if (typeof path !== 'undefined') { + object.sourcePath = path; + } + return object; + }, + + /** + * Populates an object with properties of another object + * @static + * @memberOf fabric.util + * @param {Object} source Source object + * @param {Object} destination Destination object + * @return {Array} properties Properties names to include + */ + populateWithProperties: function(source, destination, properties) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + for (var i = 0, len = properties.length; i < len; i++) { + if (properties[i] in source) { + destination[properties[i]] = source[properties[i]]; + } + } + } + }, + + /** + * WARNING: THIS WAS TO SUPPORT OLD BROWSERS. deprecated. + * WILL BE REMOVED IN FABRIC 5.0 + * Draws a dashed line between two points + * + * This method is used to draw dashed line around selection area. + * See dotted stroke in canvas + * + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x start x coordinate + * @param {Number} y start y coordinate + * @param {Number} x2 end x coordinate + * @param {Number} y2 end y coordinate + * @param {Array} da dash array pattern + * @deprecated + */ + drawDashedLine: function(ctx, x, y, x2, y2, da) { + var dx = x2 - x, + dy = y2 - y, + len = sqrt(dx * dx + dy * dy), + rot = atan2(dy, dx), + dc = da.length, + di = 0, + draw = true; + + ctx.save(); + ctx.translate(x, y); + ctx.moveTo(0, 0); + ctx.rotate(rot); + + x = 0; + while (len > x) { + x += da[di++ % dc]; + if (x > len) { + x = len; + } + ctx[draw ? 'lineTo' : 'moveTo'](x, 0); + draw = !draw; + } + + ctx.restore(); + }, + + /** + * Creates canvas element + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element + */ + createCanvasElement: function() { + return fabric.document.createElement('canvas'); + }, + + /** + * Creates a canvas element that is a copy of another and is also painted + * @param {CanvasElement} canvas to copy size and content of + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element + */ + copyCanvasElement: function(canvas) { + var newCanvas = fabric.util.createCanvasElement(); + newCanvas.width = canvas.width; + newCanvas.height = canvas.height; + newCanvas.getContext('2d').drawImage(canvas, 0, 0); + return newCanvas; + }, + + /** + * since 2.6.0 moved from canvas instance to utility. + * @param {CanvasElement} canvasEl to copy size and content of + * @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too + * @param {Number} quality <= 1 and > 0 + * @static + * @memberOf fabric.util + * @return {String} data url + */ + toDataURL: function(canvasEl, format, quality) { + return canvasEl.toDataURL('image/' + format, quality); + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + createImage: function() { + return fabric.document.createElement('img'); + }, + + /** + * Multiply matrix A by matrix B to nest transformations + * @static + * @memberOf fabric.util + * @param {Array} a First transformMatrix + * @param {Array} b Second transformMatrix + * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(a, b, is2x2) { + // Matrix multiply a * b + return [ + a[0] * b[0] + a[2] * b[1], + a[1] * b[0] + a[3] * b[1], + a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], + is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], + is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] + ]; + }, + + /** + * Decomposes standard 2x3 matrix into transform components + * @static + * @memberOf fabric.util + * @param {Array} a transformMatrix + * @return {Object} Components of transform + */ + qrDecompose: function(a) { + var angle = atan2(a[1], a[0]), + denom = pow(a[0], 2) + pow(a[1], 2), + scaleX = sqrt(denom), + scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX, + skewX = atan2(a[0] * a[2] + a[1] * a [3], denom); + return { + angle: angle / PiBy180, + scaleX: scaleX, + scaleY: scaleY, + skewX: skewX / PiBy180, + skewY: 0, + translateX: a[4], + translateY: a[5] + }; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] angle in degrees + * @return {Number[]} transform matrix + */ + calcRotateMatrix: function(options) { + if (!options.angle) { + return fabric.iMatrix.concat(); + } + var theta = fabric.util.degreesToRadians(options.angle), + cos = fabric.util.cos(theta), + sin = fabric.util.sin(theta); + return [cos, sin, -sin, cos, 0, 0]; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet. + * is called DimensionsTransformMatrix because those properties are the one that influence + * the size of the resulting box of the object. + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @return {Number[]} transform matrix + */ + calcDimensionsMatrix: function(options) { + var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, + scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, + scaleMatrix = [ + options.flipX ? -scaleX : scaleX, + 0, + 0, + options.flipY ? -scaleY : scaleY, + 0, + 0], + multiply = fabric.util.multiplyTransformMatrices, + degreesToRadians = fabric.util.degreesToRadians; + if (options.skewX) { + scaleMatrix = multiply( + scaleMatrix, + [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], + true); + } + if (options.skewY) { + scaleMatrix = multiply( + scaleMatrix, + [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], + true); + } + return scaleMatrix; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @param {Number} [options.translateX] + * @param {Number} [options.translateY] + * @return {Number[]} transform matrix + */ + composeMatrix: function(options) { + var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], + multiply = fabric.util.multiplyTransformMatrices; + if (options.angle) { + matrix = multiply(matrix, fabric.util.calcRotateMatrix(options)); + } + if (options.scaleX !== 1 || options.scaleY !== 1 || + options.skewX || options.skewY || options.flipX || options.flipY) { + matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options)); + } + return matrix; + }, + + /** + * reset an object transform state to neutral. Top and left are not accounted for + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to transform + */ + resetObjectTransform: function (target) { + target.scaleX = 1; + target.scaleY = 1; + target.skewX = 0; + target.skewY = 0; + target.flipX = false; + target.flipY = false; + target.rotate(0); + }, + + /** + * Extract Object transform values + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to read from + * @return {Object} Components of transform + */ + saveObjectTransform: function (target) { + return { + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + angle: target.angle, + left: target.left, + flipX: target.flipX, + flipY: target.flipY, + top: target.top + }; + }, + + /** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Number} tolerance Tolerance + */ + isTransparent: function(ctx, x, y, tolerance) { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + var _isTransparent = true, i, temp, + imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1), + l = imageData.data.length; + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (i = 3; i < l; i += 4) { + temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) { + break; // Stop if colour found + } + } + + imageData = null; + + return _isTransparent; + }, + + /** + * Parse preserveAspectRatio attribute from element + * @param {string} attribute to be parsed + * @return {Object} an object containing align and meetOrSlice attribute + */ + parsePreserveAspectRatioAttribute: function(attribute) { + var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid', + aspectRatioAttrs = attribute.split(' '), align; + + if (aspectRatioAttrs && aspectRatioAttrs.length) { + meetOrSlice = aspectRatioAttrs.pop(); + if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') { + align = meetOrSlice; + meetOrSlice = 'meet'; + } + else if (aspectRatioAttrs.length) { + align = aspectRatioAttrs.pop(); + } + } + //divide align in alignX and alignY + alignX = align !== 'none' ? align.slice(1, 4) : 'none'; + alignY = align !== 'none' ? align.slice(5, 8) : 'none'; + return { + meetOrSlice: meetOrSlice, + alignX: alignX, + alignY: alignY + }; + }, + + /** + * Clear char widths cache for the given font family or all the cache if no + * fontFamily is specified. + * Use it if you know you are loading fonts in a lazy way and you are not waiting + * for custom fonts to load properly when adding text objects to the canvas. + * If a text object is added when its own font is not loaded yet, you will get wrong + * measurement and so wrong bounding boxes. + * After the font cache is cleared, either change the textObject text content or call + * initDimensions() to trigger a recalculation + * @memberOf fabric.util + * @param {String} [fontFamily] font family to clear + */ + clearFabricFontCache: function(fontFamily) { + fontFamily = (fontFamily || '').toLowerCase(); + if (!fontFamily) { + fabric.charWidthsCache = { }; + } + else if (fabric.charWidthsCache[fontFamily]) { + delete fabric.charWidthsCache[fontFamily]; + } + }, + + /** + * Given current aspect ratio, determines the max width and height that can + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Number} ar aspect ratio + * @param {Number} maximumArea Maximum area you want to achieve + * @return {Object.x} Limited dimensions by X + * @return {Object.y} Limited dimensions by Y + */ + limitDimsByArea: function(ar, maximumArea) { + var roughWidth = Math.sqrt(maximumArea * ar), + perfLimitSizeY = Math.floor(maximumArea / roughWidth); + return { x: Math.floor(roughWidth), y: perfLimitSizeY }; + }, + + capValue: function(min, value, max) { + return Math.max(min, Math.min(value, max)); + }, + + /** + * Finds the scale for the object source to fit inside the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to fit into destination + */ + findScaleToFit: function(source, destination) { + return Math.min(destination.width / source.width, destination.height / source.height); + }, + + /** + * Finds the scale for the object source to cover entirely the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to cover destination + */ + findScaleToCover: function(source, destination) { + return Math.max(destination.width / source.width, destination.height / source.height); + }, + + /** + * given an array of 6 number returns something like `"matrix(...numbers)"` + * @memberOf fabric.util + * @param {Array} transform an array with 6 numbers + * @return {String} transform matrix for svg + * @return {Object.y} Limited dimensions by Y + */ + matrixToSVG: function(transform) { + return 'matrix(' + transform.map(function(value) { + return fabric.util.toFixed(value, fabric.Object.NUM_FRACTION_DIGITS); + }).join(' ') + ')'; + }, + + /** + * given an object and a transform, apply the inverse transform to the object, + * this is equivalent to remove from that object that transformation, so that + * added in a space with the removed transform, the object will be the same as before. + * Removing from an object a transform that scale by 2 is like scaling it by 1/2. + * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg + * in the opposite direction. + * This util is used to add objects inside transformed groups or nested groups. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + removeTransformFromObject: function(object, transform) { + var inverted = fabric.util.invertTransform(transform), + finalTransform = fabric.util.multiplyTransformMatrices(inverted, object.calcOwnMatrix()); + fabric.util.applyTransformToObject(object, finalTransform); + }, + + /** + * given an object and a transform, apply the transform to the object. + * this is equivalent to change the space where the object is drawn. + * Adding to an object a transform that scale by 2 is like scaling it by 2. + * This is used when removing an object from an active selection for example. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + addTransformToObject: function(object, transform) { + fabric.util.applyTransformToObject( + object, + fabric.util.multiplyTransformMatrices(transform, object.calcOwnMatrix()) + ); + }, + + /** + * discard an object transform state and apply the one from the matrix. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + applyTransformToObject: function(object, transform) { + var options = fabric.util.qrDecompose(transform), + center = new fabric.Point(options.translateX, options.translateY); + object.flipX = false; + object.flipY = false; + object.set('scaleX', options.scaleX); + object.set('scaleY', options.scaleY); + object.skewX = options.skewX; + object.skewY = options.skewY; + object.angle = options.angle; + object.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * given a width and height, return the size of the bounding box + * that can contains the box with width/height with applied transform + * described in options. + * Use to calculate the boxes around objects for controls. + * @memberOf fabric.util + * @param {Number} width + * @param {Number} height + * @param {Object} options + * @param {Number} options.scaleX + * @param {Number} options.scaleY + * @param {Number} options.skewX + * @param {Number} options.skewY + * @return {Object.x} width of containing + * @return {Object.y} height of containing + */ + sizeAfterTransform: function(width, height, options) { + var dimX = width / 2, dimY = height / 2, + points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + transformMatrix = fabric.util.calcDimensionsMatrix(options), + bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); + return { + x: bbox.width, + y: bbox.height, + }; + } + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + var _join = Array.prototype.join, + commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }, + repeatedCommands = { + m: 'l', + M: 'L' + }; + function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { + var costh2 = fabric.util.cos(th2), + sinth2 = fabric.util.sin(th2), + costh3 = fabric.util.cos(th3), + sinth3 = fabric.util.sin(th3), + toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1, + toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1, + cp1X = fromX + mT * ( -cosTh * rx * sinth2 - sinTh * ry * costh2), + cp1Y = fromY + mT * ( -sinTh * rx * sinth2 + cosTh * ry * costh2), + cp2X = toX + mT * ( cosTh * rx * sinth3 + sinTh * ry * costh3), + cp2Y = toY + mT * ( sinTh * rx * sinth3 - cosTh * ry * costh3); + + return ['C', + cp1X, cp1Y, + cp2X, cp2Y, + toX, toY + ]; + } + + /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp + * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here + * http://mozilla.org/MPL/2.0/ + */ + function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) { + var PI = Math.PI, th = rotateX * PI / 180, + sinTh = fabric.util.sin(th), + cosTh = fabric.util.cos(th), + fromX = 0, fromY = 0; + + rx = Math.abs(rx); + ry = Math.abs(ry); + + var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5, + py = -cosTh * toY * 0.5 + sinTh * toX * 0.5, + rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px, + pl = rx2 * ry2 - rx2 * py2 - ry2 * px2, + root = 0; + + if (pl < 0) { + var s = Math.sqrt(1 - pl / (rx2 * ry2)); + rx *= s; + ry *= s; + } + else { + root = (large === sweep ? -1.0 : 1.0) * + Math.sqrt( pl / (rx2 * py2 + ry2 * px2)); + } + + var cx = root * rx * py / ry, + cy = -root * ry * px / rx, + cx1 = cosTh * cx - sinTh * cy + toX * 0.5, + cy1 = sinTh * cx + cosTh * cy + toY * 0.5, + mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry), + dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry); + + if (sweep === 0 && dtheta > 0) { + dtheta -= 2 * PI; + } + else if (sweep === 1 && dtheta < 0) { + dtheta += 2 * PI; + } + + // Convert into cubic bezier segments <= 90deg + var segments = Math.ceil(Math.abs(dtheta / PI * 2)), + result = [], mDelta = dtheta / segments, + mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2), + th3 = mTheta + mDelta; + + for (var i = 0; i < segments; i++) { + result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY); + fromX = result[i][5]; + fromY = result[i][6]; + mTheta = th3; + th3 += mDelta; + } + return result; + } + + /* + * Private + */ + function calcVectorAngle(ux, uy, vx, vy) { + var ta = Math.atan2(uy, ux), + tb = Math.atan2(vy, vx); + if (tb >= ta) { + return tb - ta; + } + else { + return 2 * Math.PI - (ta - tb); + } + } + + /** + * Calculate bounding box of a beziercurve + * @param {Number} x0 starting point + * @param {Number} y0 + * @param {Number} x1 first control point + * @param {Number} y1 + * @param {Number} x2 secondo control point + * @param {Number} y2 + * @param {Number} x3 end of bezier + * @param {Number} y3 + */ + // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. + // TODO: can we normalize this with the starting points set at 0 and then translated the bbox? + function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { + var argsString; + if (fabric.cachesBoundsOfCurve) { + argsString = _join.call(arguments); + if (fabric.boundsOfCurveCache[argsString]) { + return fabric.boundsOfCurveCache[argsString]; + } + } + + var sqrt = Math.sqrt, + min = Math.min, max = Math.max, + abs = Math.abs, tvalues = [], + bounds = [[], []], + a, b, c, t, t1, t2, b2ac, sqrtb2ac; + + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; + + for (var i = 0; i < 2; ++i) { + if (i > 0) { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } + + if (abs(a) < 1e-12) { + if (abs(b) < 1e-12) { + continue; + } + t = -c / b; + if (0 < t && t < 1) { + tvalues.push(t); + } + continue; + } + b2ac = b * b - 4 * c * a; + if (b2ac < 0) { + continue; + } + sqrtb2ac = sqrt(b2ac); + t1 = (-b + sqrtb2ac) / (2 * a); + if (0 < t1 && t1 < 1) { + tvalues.push(t1); + } + t2 = (-b - sqrtb2ac) / (2 * a); + if (0 < t2 && t2 < 1) { + tvalues.push(t2); + } + } + + var x, y, j = tvalues.length, jlen = j, mt; + while (j--) { + t = tvalues[j]; + mt = 1 - t; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; + + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; + } + + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; + var result = [ + { + x: min.apply(null, bounds[0]), + y: min.apply(null, bounds[1]) + }, + { + x: max.apply(null, bounds[0]), + y: max.apply(null, bounds[1]) + } + ]; + if (fabric.cachesBoundsOfCurve) { + fabric.boundsOfCurveCache[argsString] = result; + } + return result; + } + + /** + * Converts arc to a bunch of bezier curves + * @param {Number} fx starting point x + * @param {Number} fy starting point y + * @param {Array} coords Arc command + */ + function fromArcToBeziers(fx, fy, coords) { + var rx = coords[1], + ry = coords[2], + rot = coords[3], + large = coords[4], + sweep = coords[5], + tx = coords[6], + ty = coords[7], + segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segsNorm.length; i < len; i++) { + segsNorm[i][1] += fx; + segsNorm[i][2] += fy; + segsNorm[i][3] += fx; + segsNorm[i][4] += fy; + segsNorm[i][5] += fx; + segsNorm[i][6] += fy; + } + return segsNorm; + }; + + /** + * This function take a parsed SVG path and make it simpler for fabricJS logic. + * simplification consist of: only UPPERCASE absolute commands ( relative converted to absolute ) + * S converted in C, T converted in Q, A converted in C. + * @param {Array} path the array of commands of a parsed svg path for fabric.Path + * @return {Array} the simplified array of commands of a parsed svg path for fabric.Path + */ + function makePathSimpler(path) { + // x and y represent the last point of the path. the previous command point. + // we add them to each relative command to make it an absolute comment. + // we also swap the v V h H with L, because are easier to transform. + var x = 0, y = 0, len = path.length, + // x1 and y1 represent the last point of the subpath. the subpath is started with + // m or M command. When a z or Z command is drawn, x and y need to be resetted to + // the last x1 and y1. + x1 = 0, y1 = 0, current, i, converted, + // previous will host the letter of the previous command, to handle S and T. + // controlX and controlY will host the previous reflected control point + destinationPath = [], previous, controlX, controlY; + for (i = 0; i < len; ++i) { + converted = false; + current = path[i].slice(0); + switch (current[0]) { // first letter + case 'l': // lineto, relative + current[0] = 'L'; + current[1] += x; + current[2] += y; + // falls through + case 'L': + x = current[1]; + y = current[2]; + break; + case 'h': // horizontal lineto, relative + current[1] += x; + // falls through + case 'H': + current[0] = 'L'; + current[2] = y; + x = current[1]; + break; + case 'v': // vertical lineto, relative + current[1] += y; + // falls through + case 'V': + current[0] = 'L'; + y = current[1]; + current[1] = x; + current[2] = y; + break; + case 'm': // moveTo, relative + current[0] = 'M'; + current[1] += x; + current[2] += y; + // falls through + case 'M': + x = current[1]; + y = current[2]; + x1 = current[1]; + y1 = current[2]; + break; + case 'c': // bezierCurveTo, relative + current[0] = 'C'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + current[5] += x; + current[6] += y; + // falls through + case 'C': + controlX = current[3]; + controlY = current[4]; + x = current[5]; + y = current[6]; + break; + case 's': // shorthand cubic bezierCurveTo, relative + current[0] = 'S'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'S': + // would be sScC but since we are swapping sSc for C, we check just that. + if (previous === 'C') { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + x = current[3]; + y = current[4]; + current[0] = 'C'; + current[5] = current[3]; + current[6] = current[4]; + current[3] = current[1]; + current[4] = current[2]; + current[1] = controlX; + current[2] = controlY; + // current[3] and current[4] are NOW the second control point. + // we keep it for the next reflection. + controlX = current[3]; + controlY = current[4]; + break; + case 'q': // quadraticCurveTo, relative + current[0] = 'Q'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'Q': + controlX = current[1]; + controlY = current[2]; + x = current[3]; + y = current[4]; + break; + case 't': // shorthand quadraticCurveTo, relative + current[0] = 'T'; + current[1] += x; + current[2] += y; + // falls through + case 'T': + if (previous === 'Q') { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + current[0] = 'Q'; + x = current[1]; + y = current[2]; + current[1] = controlX; + current[2] = controlY; + current[3] = x; + current[4] = y; + break; + case 'a': + current[0] = 'A'; + current[6] += x; + current[7] += y; + // falls through + case 'A': + converted = true; + destinationPath = destinationPath.concat(fromArcToBeziers(x, y, current)); + x = current[6]; + y = current[7]; + break; + case 'z': + case 'Z': + x = x1; + y = y1; + break; + default: + } + if (!converted) { + destinationPath.push(current); + } + previous = current[0]; + } + return destinationPath; + }; + + /** + * Calc length from point x1,y1 to x2,y2 + * @param {Number} x1 starting point x + * @param {Number} y1 starting point y + * @param {Number} x2 starting point x + * @param {Number} y2 starting point y + * @return {Number} length of segment + */ + function calcLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + + // functions for the Cubic beizer + // taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350 + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + + function getPointOnCubicBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function(pct) { + var c1 = CB1(pct), c2 = CB2(pct), c3 = CB3(pct), c4 = CB4(pct); + return { + x: p4x * c1 + p3x * c2 + p2x * c3 + p1x * c4, + y: p4y * c1 + p3y * c2 + p2y * c3 + p1y * c4 + }; + }; + } + + function getTangentCubicIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (3 * invT * invT * (p2x - p1x)) + (6 * invT * pct * (p3x - p2x)) + + (3 * pct * pct * (p4x - p3x)), + tangentY = (3 * invT * invT * (p2y - p1y)) + (6 * invT * pct * (p3y - p2y)) + + (3 * pct * pct * (p4y - p3y)); + return Math.atan2(tangentY, tangentX); + }; + } + + function QB1(t) { + return t * t; + } + + function QB2(t) { + return 2 * t * (1 - t); + } + + function QB3(t) { + return (1 - t) * (1 - t); + } + + function getPointOnQuadraticBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function(pct) { + var c1 = QB1(pct), c2 = QB2(pct), c3 = QB3(pct); + return { + x: p3x * c1 + p2x * c2 + p1x * c3, + y: p3y * c1 + p2y * c2 + p1y * c3 + }; + }; + } + + function getTangentQuadraticIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (2 * invT * (p2x - p1x)) + (2 * pct * (p3x - p2x)), + tangentY = (2 * invT * (p2y - p1y)) + (2 * pct * (p3y - p2y)); + return Math.atan2(tangentY, tangentX); + }; + } + + + // this will run over a path segment ( a cubic or quadratic segment) and approximate it + // with 100 segemnts. This will good enough to calculate the length of the curve + function pathIterator(iterator, x1, y1) { + var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc; + for (perc = 1; perc <= 100; perc += 1) { + p = iterator(perc / 100); + tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y); + tempP = p; + } + return tmpLen; + } + + /** + * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1 + * that correspond to that pixels run over the path. + * The percentage will be then used to find the correct point on the canvas for the path. + * @param {Array} segInfo fabricJS collection of information on a parsed path + * @param {Number} distance from starting point, in pixels. + * @return {Object} info object with x and y ( the point on canvas ) and angle, the tangent on that point; + */ + function findPercentageForDistance(segInfo, distance) { + var perc = 0, tmpLen = 0, iterator = segInfo.iterator, tempP = { x: segInfo.x, y: segInfo.y }, + p, nextLen, nextStep = 0.01, angleFinder = segInfo.angleFinder, lastPerc; + // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100 + // the path + while (tmpLen < distance && perc <= 1 && nextStep > 0.0001) { + p = iterator(perc); + lastPerc = perc; + nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y); + // compare tmpLen each cycle with distance, decide next perc to test. + if ((nextLen + tmpLen) > distance) { + // we discard this step and we make smaller steps. + nextStep /= 2; + perc -= nextStep; + } + else { + tempP = p; + perc += nextStep; + tmpLen += nextLen; + } + } + p.angle = angleFinder(lastPerc); + return p; + } + + /** + * Run over a parsed and simplifed path and extrac some informations. + * informations are length of each command and starting point + * @param {Array} path fabricJS parsed path commands + * @return {Array} path commands informations + */ + function getPathSegmentsInfo(path) { + var totalLength = 0, len = path.length, current, + //x2 and y2 are the coords of segment start + //x1 and y1 are the coords of the current point + x1 = 0, y1 = 0, x2 = 0, y2 = 0, info = [], iterator, tempInfo, angleFinder; + for (var i = 0; i < len; i++) { + current = path[i]; + tempInfo = { + x: x1, + y: y1, + command: current[0], + }; + switch (current[0]) { //first letter + case 'M': + tempInfo.length = 0; + x2 = x1 = current[1]; + y2 = y1 = current[2]; + break; + case 'L': + tempInfo.length = calcLineLength(x1, y1, current[1], current[2]); + x1 = current[1]; + y1 = current[2]; + break; + case 'C': + iterator = getPointOnCubicBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + angleFinder = getTangentCubicIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[5]; + y1 = current[6]; + break; + case 'Q': + iterator = getPointOnQuadraticBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + angleFinder = getTangentQuadraticIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[3]; + y1 = current[4]; + break; + case 'Z': + case 'z': + // we add those in order to ease calculations later + tempInfo.destX = x2; + tempInfo.destY = y2; + tempInfo.length = calcLineLength(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + break; + } + totalLength += tempInfo.length; + info.push(tempInfo); + } + info.push({ length: totalLength, x: x1, y: y1 }); + return info; + } + + function getPointOnPath(path, distance, infos) { + if (!infos) { + infos = getPathSegmentsInfo(path); + } + var i = 0; + while ((distance - infos[i].length > 0) && i < (infos.length - 2)) { + distance -= infos[i].length; + i++; + } + // var distance = infos[infos.length - 1] * perc; + var segInfo = infos[i], segPercent = distance / segInfo.length, + command = segInfo.command, segment = path[i], info; + + switch (command) { + case 'M': + return { x: segInfo.x, y: segInfo.y, angle: 0 }; + case 'Z': + case 'z': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segInfo.destX, segInfo.destY), + segPercent + ); + info.angle = Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x); + return info; + case 'L': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segment[1], segment[2]), + segPercent + ); + info.angle = Math.atan2(segment[2] - segInfo.y, segment[1] - segInfo.x); + return info; + case 'C': + return findPercentageForDistance(segInfo, distance); + case 'Q': + return findPercentageForDistance(segInfo, distance); + } + } + + /** + * + * @param {string} pathString + * @return {(string|number)[][]} An array of SVG path commands + * @example Usage + * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [ + * ['M', 3, 4], + * ['Q', 3, 5, 2, 1, 4, 0], + * ['Q', 9, 12, 2, 1, 4, 0], + * ]; + * + */ + function parsePath(pathString) { + var result = [], + coords = [], + currentPath, + parsed, + re = fabric.rePathCommand, + rNumber = '[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?\\s*', + rNumberCommaWsp = '(' + rNumber + ')' + fabric.commaWsp, + rFlagCommaWsp = '([01])' + fabric.commaWsp + '?', + rArcSeq = rNumberCommaWsp + '?' + rNumberCommaWsp + '?' + rNumberCommaWsp + rFlagCommaWsp + rFlagCommaWsp + + rNumberCommaWsp + '?(' + rNumber + ')', + regArcArgumentSequence = new RegExp(rArcSeq, 'g'), + match, + coordsStr, + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + path; + if (!pathString || !pathString.match) { + return result; + } + path = pathString.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + for (var i = 0, coordsParsed, len = path.length; i < len; i++) { + currentPath = path[i]; + + coordsStr = currentPath.slice(1).trim(); + coords.length = 0; + + var command = currentPath.charAt(0); + coordsParsed = [command]; + + if (command.toLowerCase() === 'a') { + // arcs have special flags that apparently don't require spaces so handle special + for (var args; (args = regArcArgumentSequence.exec(coordsStr));) { + for (var j = 1; j < args.length; j++) { + coords.push(args[j]); + } + } + } + else { + while ((match = re.exec(coordsStr))) { + coords.push(match[0]); + } + } + + for (var j = 0, jlen = coords.length; j < jlen; j++) { + parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + coordsParsed.push(parsed); + } + } + + var commandLength = commandLengths[command.toLowerCase()], + repeatedCommand = repeatedCommands[command] || command; + + if (coordsParsed.length - 1 > commandLength) { + for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { + result.push([command].concat(coordsParsed.slice(k, k + commandLength))); + command = repeatedCommand; + } + } + else { + result.push(coordsParsed); + } + } + + return result; + }; + + /** + * + * Converts points to a smooth SVG path + * @param {{ x: number,y: number }[]} points Array of points + * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value. + * @return {(string|number)[][]} An array of SVG path commands + */ + function getSmoothPathFromPoints(points, correction) { + var path = [], i, + p1 = new fabric.Point(points[0].x, points[0].y), + p2 = new fabric.Point(points[1].x, points[1].y), + len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; + correction = correction || 0; + + if (manyPoints) { + multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; + multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; + } + path.push(['M', p1.x - multSignX * correction, p1.y - multSignY * correction]); + for (i = 1; i < len; i++) { + if (!p1.eq(p2)) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]); + } + p1 = points[i]; + if ((i + 1) < points.length) { + p2 = points[i + 1]; + } + } + if (manyPoints) { + multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; + multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; + } + path.push(['L', p1.x + multSignX * correction, p1.y + multSignY * correction]); + return path; + } + /** + * Transform a path by transforming each segment. + * it has to be a simplified path or it won't work. + * WARNING: this depends from pathOffset for correct operation + * @param {Array} path fabricJS parsed and simplified path commands + * @param {Array} transform matrix that represent the transformation + * @param {Object} [pathOffset] the fabric.Path pathOffset + * @param {Number} pathOffset.x + * @param {Number} pathOffset.y + * @returns {Array} the transformed path + */ + function transformPath(path, transform, pathOffset) { + if (pathOffset) { + transform = fabric.util.multiplyTransformMatrices( + transform, + [1, 0, 0, 1, -pathOffset.x, -pathOffset.y] + ); + } + return path.map(function(pathSegment) { + var newSegment = pathSegment.slice(0), point = {}; + for (var i = 1; i < pathSegment.length - 1; i += 2) { + point.x = pathSegment[i]; + point.y = pathSegment[i + 1]; + point = fabric.util.transformPoint(point, transform); + newSegment[i] = point.x; + newSegment[i + 1] = point.y; + } + return newSegment; + }); + } + + /** + * Calculate bounding box of a elliptic-arc + * @deprecated + * @param {Number} fx start point of arc + * @param {Number} fy + * @param {Number} rx horizontal radius + * @param {Number} ry vertical radius + * @param {Number} rot angle of horizontal axis + * @param {Number} large 1 or 0, whatever the arc is the big or the small on the 2 points + * @param {Number} sweep 1 or 0, 1 clockwise or counterclockwise direction + * @param {Number} tx end point of arc + * @param {Number} ty + */ + function getBoundsOfArc(fx, fy, rx, ry, rot, large, sweep, tx, ty) { + + var fromX = 0, fromY = 0, bound, bounds = [], + segs = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segs.length; i < len; i++) { + bound = getBoundsOfCurve(fromX, fromY, segs[i][1], segs[i][2], segs[i][3], segs[i][4], segs[i][5], segs[i][6]); + bounds.push({ x: bound[0].x + fx, y: bound[0].y + fy }); + bounds.push({ x: bound[1].x + fx, y: bound[1].y + fy }); + fromX = segs[i][5]; + fromY = segs[i][6]; + } + return bounds; + }; + + /** + * Draws arc + * @deprecated + * @param {CanvasRenderingContext2D} ctx + * @param {Number} fx + * @param {Number} fy + * @param {Array} coords coords of the arc, without the front 'A/a' + */ + function drawArc(ctx, fx, fy, coords) { + coords = coords.slice(0).unshift('X'); // command A or a does not matter + var beziers = fromArcToBeziers(fx, fy, coords); + beziers.forEach(function(bezier) { + ctx.bezierCurveTo.apply(ctx, bezier.slice(1)); + }); + }; + + /** + * Join path commands to go back to svg format + * @param {Array} pathData fabricJS parsed path commands + * @return {String} joined path 'M 0 0 L 20 30' + */ + fabric.util.joinPath = function(pathData) { + return pathData.map(function (segment) { return segment.join(' '); }).join(' '); + }; + fabric.util.parsePath = parsePath; + fabric.util.makePathSimpler = makePathSimpler; + fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints; + fabric.util.getPathSegmentsInfo = getPathSegmentsInfo; + fabric.util.getBoundsOfCurve = getBoundsOfCurve; + fabric.util.getPointOnPath = getPointOnPath; + fabric.util.transformPath = transformPath; + /** + * Typo of `fromArcToBeziers` kept for not breaking the api once corrected. + * Will be removed in fabric 5.0 + * @deprecated + */ + fabric.util.fromArcToBeizers = fromArcToBeziers; + // kept because we do not want to make breaking changes. + // but useless and deprecated. + fabric.util.getBoundsOfArc = getBoundsOfArc; + fabric.util.drawArc = drawArc; +})(); + + +(function() { + + var slice = Array.prototype.slice; + + /** + * Invokes method on all items in a given array + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + * @return {Array} + */ + function invoke(array, method) { + var args = slice.call(arguments, 2), result = []; + for (var i = 0, len = array.length; i < len; i++) { + result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); + } + return result; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function max(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function fill(array, value) { + var k = array.length; + while (k--) { + array[k] = value; + } + return array; + } + + /** + * @private + */ + function find(array, byProperty, condition) { + if (!array || array.length === 0) { + return; + } + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + if (byProperty) { + while (i--) { + if (condition(array[i][byProperty], result)) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (condition(array[i], result)) { + result = array[i]; + } + } + } + return result; + } + + /** + * @namespace fabric.util.array + */ + fabric.util.array = { + fill: fill, + invoke: invoke, + min: min, + max: max + }; + +})(); + + +(function() { + /** + * Copies all enumerable properties of one js object to another + * this does not and cannot compete with generic utils. + * Does not clone or extend fabric.Object subclasses. + * This is mostly for internal use and has extra handling for fabricJS objects + * it skips the canvas and group properties in deep cloning. + * @memberOf fabric.util.object + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + * @param {Boolean} [deep] Whether to extend nested objects + * @return {Object} + */ + + function extend(destination, source, deep) { + // JScript DontEnum bug is not taken care of + // the deep clone is for internal use, is not meant to avoid + // javascript traps or cloning html element or self referenced objects. + if (deep) { + if (!fabric.isLikelyNode && source instanceof Element) { + // avoid cloning deep images, canvases, + destination = source; + } + else if (source instanceof Array) { + destination = []; + for (var i = 0, len = source.length; i < len; i++) { + destination[i] = extend({ }, source[i], deep); + } + } + else if (source && typeof source === 'object') { + for (var property in source) { + if (property === 'canvas' || property === 'group') { + // we do not want to clone this props at all. + // we want to keep the keys in the copy + destination[property] = null; + } + else if (source.hasOwnProperty(property)) { + destination[property] = extend({ }, source[property], deep); + } + } + } + else { + // this sounds odd for an extend but is ok for recursive use + destination = source; + } + } + else { + for (var property in source) { + destination[property] = source[property]; + } + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * @memberOf fabric.util.object + * @param {Object} object Object to clone + * @param {Boolean} [deep] Whether to clone nested objects + * @return {Object} + */ + + //TODO: this function return an empty object if you try to clone null + function clone(object, deep) { + return extend({ }, object, deep); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + fabric.util.object.extend(fabric.util, fabric.Observable); +})(); + + +(function() { + + /** + * Camelizes a string + * @memberOf fabric.util.string + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ + function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); + } + + /** + * Capitalizes a string + * @memberOf fabric.util.string + * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. + * @return {String} Capitalized version of a string + */ + function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); + } + + /** + * Escapes XML in a string + * @memberOf fabric.util.string + * @param {String} string String to escape + * @return {String} Escaped version of a string + */ + function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + /** + * Divide a string in the user perceived single units + * @memberOf fabric.util.string + * @param {String} textstring String to escape + * @return {Array} array containing the graphemes + */ + function graphemeSplit(textstring) { + var i = 0, chr, graphemes = []; + for (i = 0, chr; i < textstring.length; i++) { + if ((chr = getWholeChar(textstring, i)) === false) { + continue; + } + graphemes.push(chr); + } + return graphemes; + } + + // taken from mdn in the charAt doc page. + function getWholeChar(str, i) { + var code = str.charCodeAt(i); + + if (isNaN(code)) { + return ''; // Position not found + } + if (code < 0xD800 || code > 0xDFFF) { + return str.charAt(i); + } + + // High surrogate (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 <= code && code <= 0xDBFF) { + if (str.length <= (i + 1)) { + throw 'High surrogate without following low surrogate'; + } + var next = str.charCodeAt(i + 1); + if (0xDC00 > next || next > 0xDFFF) { + throw 'High surrogate without following low surrogate'; + } + return str.charAt(i) + str.charAt(i + 1); + } + // Low surrogate (0xDC00 <= code && code <= 0xDFFF) + if (i === 0) { + throw 'Low surrogate without preceding high surrogate'; + } + var prev = str.charCodeAt(i - 1); + + // (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 > prev || prev > 0xDBFF) { + throw 'Low surrogate without preceding high surrogate'; + } + // We can pass over low surrogates now as the second component + // in a pair which we have already processed + return false; + } + + + /** + * String utilities + * @namespace fabric.util.string + */ + fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml, + graphemeSplit: graphemeSplit + }; +})(); + + +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }, + + IS_DONTENUM_BUGGY = (function() { + for (var p in { toString: 1 }) { + if (p === 'toString') { + return false; + } + } + return true; + })(), + + /** @ignore */ + addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + function callSuper(methodName) { + var parentMethod = null, + _this = this; + + // climb prototype chain to find method not equal to callee's method + while (_this.constructor.superclass) { + var superClassMethod = _this.constructor.superclass.prototype[methodName]; + if (_this[methodName] !== superClassMethod) { + parentMethod = superClassMethod; + break; + } + // eslint-disable-next-line + _this = _this.constructor.superclass.prototype; + } + + if (!parentMethod) { + return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); + } + + return (arguments.length > 1) + ? parentMethod.apply(this, slice.call(arguments, 1)) + : parentMethod.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param {Function} [parent] optional "Class" to inherit from + * @param {Object} [properties] Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; + } + + fabric.util.createClass = createClass; +})(); + + +(function () { + // since ie11 can use addEventListener but they do not support options, i need to check + var couldUseAttachEvent = !!fabric.document.createElement('div').attachEvent, + touchEvents = ['touchstart', 'touchmove', 'touchend']; + /** + * Adds an event listener to an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = function(element, eventName, handler, options) { + element && element.addEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + /** + * Removes an event listener from an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = function(element, eventName, handler, options) { + element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + function getTouchInfo(event) { + var touchProp = event.changedTouches; + if (touchProp && touchProp[0]) { + return touchProp[0]; + } + return event; + } + + fabric.util.getPointer = function(event) { + var element = event.target, + scroll = fabric.util.getScrollLeftTop(element), + _evt = getTouchInfo(event); + return { + x: _evt.clientX + scroll.left, + y: _evt.clientY + scroll.top + }; + }; + + fabric.util.isTouchEvent = function(event) { + return touchEvents.indexOf(event.type) > -1 || event.pointerType === 'touch'; + }; +})(); + + +(function () { + + /** + * Cross-browser wrapper for setting element's style + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {Object} styles + * @return {HTMLElement} Element that was passed as a first argument + */ + function setStyle(element, styles) { + var elementStyle = element.style; + if (!elementStyle) { + return element; + } + if (typeof styles === 'string') { + element.style.cssText += ';' + styles; + return styles.indexOf('opacity') > -1 + ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) + : element; + } + for (var property in styles) { + if (property === 'opacity') { + setOpacity(element, styles[property]); + } + else { + var normalizedProperty = (property === 'float' || property === 'cssFloat') + ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') + : property; + elementStyle[normalizedProperty] = styles[property]; + } + } + return element; + } + + var parseEl = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, + + /** @ignore */ + setOpacity = function (element) { return element; }; + + if (supportsOpacity) { + /** @ignore */ + setOpacity = function(element, value) { + element.style.opacity = value; + return element; + }; + } + else if (supportsFilters) { + /** @ignore */ + setOpacity = function(element, value) { + var es = element.style; + if (element.currentStyle && !element.currentStyle.hasLayout) { + es.zoom = 1; + } + if (reOpacity.test(es.filter)) { + value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')'); + es.filter = es.filter.replace(reOpacity, value); + } + else { + es.filter += ' alpha(opacity=' + (value * 100) + ')'; + } + return element; + }; + } + + fabric.util.setStyle = setStyle; + +})(); + + +(function() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + var sliceCanConvertNodelists, + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch (err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + + /** + * Adds class to an element + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if (element && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + function wrapElement(element, wrapper, attributes) { + if (typeof wrapper === 'string') { + wrapper = makeElement(wrapper, attributes); + } + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); + } + wrapper.appendChild(element); + return wrapper; + } + + /** + * Returns element scroll offsets + * @memberOf fabric.util + * @param {HTMLElement} element Element to operate on + * @return {Object} Object with left/top values + */ + function getScrollLeftTop(element) { + + var left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + // While loop checks (and then sets element to) .parentNode OR .host + // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, + // but the .parentNode of a root ShadowDOM node will always be null, instead + // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 + while (element && (element.parentNode || element.host)) { + + // Set element to element parent, or 'host' in case of ShadowDOM + element = element.parentNode || element.host; + + if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + + if (element.nodeType === 1 && element.style.position === 'fixed') { + break; + } + } + + return { left: left, top: top }; + } + + /** + * Returns offset for a given element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + var docElem, + doc = element && element.ownerDocument, + box = { left: 0, top: 0 }, + offset = { left: 0, top: 0 }, + scrollLeftTop, + offsetAttributes = { + borderLeftWidth: 'left', + borderTopWidth: 'top', + paddingLeft: 'left', + paddingTop: 'top' + }; + + if (!doc) { + return offset; + } + + for (var attr in offsetAttributes) { + offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; + } + + docElem = doc.documentElement; + if ( typeof element.getBoundingClientRect !== 'undefined' ) { + box = element.getBoundingClientRect(); + } + + scrollLeftTop = getScrollLeftTop(element); + + return { + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top + }; + } + + /** + * Returns style attribute value of a given element + * @memberOf fabric.util + * @param {HTMLElement} element Element to get style attribute for + * @param {String} attr Style attribute to get for element + * @return {String} Style attribute value of the given element. + */ + var getElementStyle; + if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { + getElementStyle = function(element, attr) { + var style = fabric.document.defaultView.getComputedStyle(element, null); + return style ? style[attr] : undefined; + }; + } + else { + getElementStyle = function(element, attr) { + var value = element.style[attr]; + if (!value && element.currentStyle) { + value = element.currentStyle[attr]; + } + return value; + }; + } + + (function () { + var style = fabric.document.documentElement.style, + selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = fabric.util.falseFunction; + } + if (selectProp) { + element.style[selectProp] = 'none'; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = 'on'; + } + return element; + } + + /** + * Makes element selectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + function getNodeCanvas(element) { + var impl = fabric.jsdomImplForWrapper(element); + return impl._canvas || impl._image; + }; + + function cleanUpJsdomNode(element) { + if (!fabric.isLikelyNode) { + return; + } + var impl = fabric.jsdomImplForWrapper(element); + if (impl) { + impl._image = null; + impl._canvas = null; + // unsure if necessary + impl._currentSrc = null; + impl._attributes = null; + impl._classList = null; + } + } + + function setImageSmoothing(ctx, value) { + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled + || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; + ctx.imageSmoothingEnabled = value; + } + + /** + * setImageSmoothing sets the context imageSmoothingEnabled property. + * Used by canvas and by ImageObject. + * @memberOf fabric.util + * @since 4.0.0 + * @param {HTMLRenderingContext2D} ctx to set on + * @param {Boolean} value true or false + */ + fabric.util.setImageSmoothing = setImageSmoothing; + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.addClass = addClass; + fabric.util.makeElement = makeElement; + fabric.util.wrapElement = wrapElement; + fabric.util.getScrollLeftTop = getScrollLeftTop; + fabric.util.getElementOffset = getElementOffset; + fabric.util.getNodeCanvas = getNodeCanvas; + fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; + +})(); + + +(function() { + + function addParamToUrl(url, param) { + return url + (/\?/.test(url) ? '&' : '?') + param; + } + + function emptyFn() { } + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {String} [options.parameters] parameters to append to url in GET or in body + * @param {String} [options.body] body to send with POST or PUT request + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = new fabric.window.XMLHttpRequest(), + body = options.body || options.parameters; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); + + +/** + * Wrapper around `console.log` (when available) + * @param {*} [values] Values to log + */ +fabric.log = console.log; + +/** + * Wrapper around `console.warn` (when available) + * @param {*} [values] Values to log as a warning + */ +fabric.warn = console.warn; + + +(function() { + + function noop() { + return false; + } + + function defaultEasing(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change (in ms) + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ + function animate(options) { + var cancel = false; + requestAnimFrame(function(timestamp) { + options || (options = { }); + + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || noop, + abort = options.abort || noop, + onComplete = options.onComplete || noop, + easing = options.easing || defaultEasing, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick(ticktime) { + // TODO: move abort call after calculation + // and pass (current,valuePerc, timePerc) as arguments + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start), + timePerc = currentTime / duration, + current = easing(currentTime, startValue, byValue, duration), + valuePerc = Math.abs((current - startValue) / byValue); + if (cancel) { + return; + } + if (abort(current, valuePerc, timePerc)) { + // remove this in 4.0 + // does to even make sense to abort and run onComplete? + onComplete(endValue, 1, 1); + return; + } + if (time > finish) { + onChange(endValue, 1, 1); + onComplete(endValue, 1, 1); + return; + } + else { + onChange(current, valuePerc, timePerc); + requestAnimFrame(tick); + } + })(start); + }); + return function() { + cancel = true; + }; + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback) { + return fabric.window.setTimeout(callback, 1000 / 60); + }; + + var _cancelAnimFrame = fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; + + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + function requestAnimFrame() { + return _requestAnimFrame.apply(fabric.window, arguments); + } + + function cancelAnimFrame() { + return _cancelAnimFrame.apply(fabric.window, arguments); + } + + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.cancelAnimFrame = cancelAnimFrame; +})(); + + +(function() { + // Calculate an in-between color. Returns a "rgba()" string. + // Credit: Edwin Martin + // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js + function calculateColor(begin, end, pos) { + var color = 'rgba(' + + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' + + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' + + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); + + color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); + color += ')'; + return color; + } + + /** + * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {String} fromColor The starting color in hex or rgb(a) format. + * @param {String} toColor The starting color in hex or rgb(a) format. + * @param {Number} [duration] Duration of change (in ms). + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ + function animateColor(fromColor, toColor, duration, options) { + var startColor = new fabric.Color(fromColor).getSource(), + endColor = new fabric.Color(toColor).getSource(), + originalOnComplete = options.onComplete, + originalOnChange = options.onChange; + options = options || {}; + + return fabric.util.animate(fabric.util.object.extend(options, { + duration: duration || 500, + startValue: startColor, + endValue: endColor, + byValue: endColor, + easing: function (currentTime, startValue, byValue, duration) { + var posValue = options.colorEasing + ? options.colorEasing(currentTime, duration) + : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); + return calculateColor(startValue, byValue, posValue); + }, + // has to take in account for color restoring; + onComplete: function(current, valuePerc, timePerc) { + if (originalOnComplete) { + return originalOnComplete( + calculateColor(endColor, endColor, 0), + valuePerc, + timePerc + ); + } + }, + onChange: function(current, valuePerc, timePerc) { + if (originalOnChange) { + if (Array.isArray(current)) { + return originalOnChange( + calculateColor(current, current, 0), + valuePerc, + timePerc + ); + } + originalOnChange(current, valuePerc, timePerc); + } + } + })); + } + + fabric.util.animateColor = animateColor; + +})(); + + +(function() { + + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + //handle the 0/0 case: + if (c === 0 && a === 0) { + s = p / (2 * Math.PI) * Math.asin(1); + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + } + return { a: a, c: c, p: p, s: s }; + } + + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); + } + + /** + * Cubic easing out + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c * ((t = t / d - 1) * t * t + 1) + b; + } + + /** + * Cubic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t + 2) + b; + } + + /** + * Quartic easing in + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c * (t /= d) * t * t * t + b; + } + + /** + * Quartic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t = t / d - 1) * t * t * t - 1) + b; + } + + /** + * Quartic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t + b; + } + return -c / 2 * ((t -= 2) * t * t * t - 2) + b; + } + + /** + * Quintic easing in + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + } + + /** + * Quintic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + } + + /** + * Quintic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + } + + /** + * Sinusoidal easing in + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Sinusoidal easing out + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t / d * (Math.PI / 2)) + b; + } + + /** + * Sinusoidal easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + } + + /** + * Exponential easing in + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + } + + /** + * Exponential easing out + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + } + + /** + * Exponential easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t === 0) { + return b; + } + if (t === d) { + return b + c; + } + t /= d / 2; + if (t < 1) { + return c / 2 * Math.pow(2, 10 * (t - 1)) + b; + } + return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * Circular easing in + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + } + + /** + * Circular easing out + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + } + + /** + * Circular easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; + } + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + } + + /** + * Elastic easing in + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; + } + + /** + * Elastic easing out + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; + } + + /** + * Elastic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d / 2; + if (t === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + var opts = normalize(a, c, p, s); + if (t < 1) { + return -0.5 * elastic(opts, t, d) + b; + } + return opts.a * Math.pow(2, -10 * (t -= 1)) * + Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.c + b; + } + + /** + * Backwards easing in + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * (t /= d) * t * ((s + 1) * t - s) + b; + } + + /** + * Backwards easing out + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + } + + /** + * Backwards easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + t /= d / 2; + if (t < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + } + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + } + + /** + * Bouncing easing in + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d - t, 0, c, d) + b; + } + + /** + * Bouncing easing out + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } + else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; + } + else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; + } + } + + /** + * Bouncing easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d / 2) { + return easeInBounce (t * 2, 0, c, d) * 0.5 + b; + } + return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + + /** + * Easing functions + * See Easing Equations by Robert Penner + * @namespace fabric.util.ease + */ + fabric.util.ease = { + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c * (t /= d) * t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d / 2); + if (t < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c * (t /= d) * t * t + b; + }, + + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +})(); + + +(function(global) { + + 'use strict'; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + parseUnit = fabric.util.parseUnit, + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + + svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line', + 'image', 'text'], + svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'], + svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'], + svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'], + + attributesMap = { + cx: 'left', + x: 'left', + r: 'radius', + cy: 'top', + y: 'top', + display: 'visible', + visibility: 'visible', + transform: 'transformMatrix', + 'fill-opacity': 'fillOpacity', + 'fill-rule': 'fillRule', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-style': 'fontStyle', + 'font-weight': 'fontWeight', + 'letter-spacing': 'charSpacing', + 'paint-order': 'paintFirst', + 'stroke-dasharray': 'strokeDashArray', + 'stroke-dashoffset': 'strokeDashOffset', + 'stroke-linecap': 'strokeLineCap', + 'stroke-linejoin': 'strokeLineJoin', + 'stroke-miterlimit': 'strokeMiterLimit', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'text-decoration': 'textDecoration', + 'text-anchor': 'textAnchor', + opacity: 'opacity', + 'clip-path': 'clipPath', + 'clip-rule': 'clipRule', + 'vector-effect': 'strokeUniform', + 'image-rendering': 'imageSmoothing', + }, + + colorAttributes = { + stroke: 'strokeOpacity', + fill: 'fillOpacity' + }, + + fSize = 'font-size', cPath = 'clip-path'; + + fabric.svgValidTagNamesRegEx = getSvgRegex(svgValidTagNames); + fabric.svgViewBoxElementsRegEx = getSvgRegex(svgViewBoxElements); + fabric.svgInvalidAncestorsRegEx = getSvgRegex(svgInvalidAncestors); + fabric.svgValidParentsRegEx = getSvgRegex(svgValidParents); + + fabric.cssRules = { }; + fabric.gradientDefs = { }; + fabric.clipPaths = { }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + function normalizeValue(attr, value, parentAttributes, fontSize) { + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; + + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + else if (attr === 'strokeUniform') { + return (value === 'non-scaling-stroke'); + } + else if (attr === 'strokeDashArray') { + if (value === 'none') { + value = null; + } + else { + value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); + } + } + else if (attr === 'transformMatrix') { + if (parentAttributes && parentAttributes.transformMatrix) { + value = multiplyTransformMatrices( + parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); + } + else { + value = fabric.parseTransformAttribute(value); + } + } + else if (attr === 'visible') { + value = value !== 'none' && value !== 'hidden'; + // display=none on parent element always takes precedence over child element + if (parentAttributes && parentAttributes.visible === false) { + value = false; + } + } + else if (attr === 'opacity') { + value = parseFloat(value); + if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { + value *= parentAttributes.opacity; + } + } + else if (attr === 'textAnchor' /* text-anchor */) { + value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; + } + else if (attr === 'charSpacing') { + // parseUnit returns px and we convert it to em + parsed = parseUnit(value, fontSize) / fontSize * 1000; + } + else if (attr === 'paintFirst') { + var fillIndex = value.indexOf('fill'); + var strokeIndex = value.indexOf('stroke'); + var value = 'fill'; + if (fillIndex > -1 && strokeIndex > -1 && strokeIndex < fillIndex) { + value = 'stroke'; + } + else if (fillIndex === -1 && strokeIndex > -1) { + value = 'stroke'; + } + } + else if (attr === 'href' || attr === 'xlink:href' || attr === 'font') { + return value; + } + else if (attr === 'imageSmoothing') { + return (value === 'optimizeQuality'); + } + else { + parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); + } + + return (!isArray && isNaN(parsed) ? value : parsed); + } + + /** + * @private + */ + function getSvgRegex(arr) { + return new RegExp('^(' + arr.join('|') + ')\\b', 'i'); + } + + /** + * @private + * @param {Object} attributes Array of attributes to parse + */ + function _setStrokeFillOpacity(attributes) { + for (var attr in colorAttributes) { + + if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { + continue; + } + + if (typeof attributes[attr] === 'undefined') { + if (!fabric.Object.prototype[attr]) { + continue; + } + attributes[attr] = fabric.Object.prototype[attr]; + } + + if (attributes[attr].indexOf('url(') === 0) { + continue; + } + + var color = new fabric.Color(attributes[attr]); + attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); + } + return attributes; + } + + /** + * @private + */ + function _getMultipleNodes(doc, nodeNames) { + var nodeName, nodeArray = [], nodeList, i, len; + for (i = 0, len = nodeNames.length; i < len; i++) { + nodeName = nodeNames[i]; + nodeList = doc.getElementsByTagName(nodeName); + nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); + } + return nodeArray; + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix + */ + fabric.parseTransformAttribute = (function() { + function rotateMatrix(matrix, args) { + var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), + x = 0, y = 0; + if (args.length === 3) { + x = args[1]; + y = args[2]; + } + + matrix[0] = cos; + matrix[1] = sin; + matrix[2] = -sin; + matrix[3] = cos; + matrix[4] = x - (cos * x - sin * y); + matrix[5] = y - (sin * x + cos * y); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewMatrix(matrix, args, pos) { + matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); + } + + function translateMatrix(matrix, args) { + matrix[4] = args[0]; + if (args.length === 2) { + matrix[5] = args[1]; + } + } + + // identity matrix + var iMatrix = fabric.iMatrix, + + // == begin transform regexp + number = fabric.reNum, + + commaWsp = fabric.commaWsp, + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + ')' + + commaWsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', + + transformList = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transformList), + // == end transform regexp + + reTransform = new RegExp(transform, 'g'); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(), + matrices = []; + + // return if no argument was given or + // an argument does not match transform attribute regexp + if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { + return matrix; + } + + attributeValue.replace(reTransform, function(match) { + + var m = new RegExp(transform).exec(match).filter(function (match) { + // match !== '' && match != null + return (!!match); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch (operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + args[0] = fabric.util.degreesToRadians(args[0]); + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewMatrix(matrix, args, 2); + break; + case 'skewY': + skewMatrix(matrix, args, 1); + break; + case 'matrix': + matrix = args; + break; + } + + // snapshot current matrix into matrices array + matrices.push(matrix.concat()); + // reset + matrix = iMatrix.concat(); + }); + + var combinedMatrix = matrices[0]; + while (matrices.length > 1) { + matrices.shift(); + combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); + } + return combinedMatrix; + }; + })(); + + /** + * @private + */ + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); + + attr = pair[0].trim().toLowerCase(); + value = pair[1].trim(); + + oStyle[attr] = value; + }); + } + + /** + * @private + */ + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') { + continue; + } + + attr = prop.toLowerCase(); + value = style[prop]; + + oStyle[attr] = value; + } + } + + /** + * @private + */ + function getGlobalStylesForElement(element, svgUid) { + var styles = { }; + for (var rule in fabric.cssRules[svgUid]) { + if (elementMatchesRule(element, rule.split(' '))) { + for (var property in fabric.cssRules[svgUid][rule]) { + styles[property] = fabric.cssRules[svgUid][rule][property]; + } + } + } + return styles; + } + + /** + * @private + */ + function elementMatchesRule(element, selectors) { + var firstMatching, parentMatching = true; + //start from rightmost selector. + firstMatching = selectorMatches(element, selectors.pop()); + if (firstMatching && selectors.length) { + parentMatching = doesSomeParentMatch(element, selectors); + } + return firstMatching && parentMatching && (selectors.length === 0); + } + + function doesSomeParentMatch(element, selectors) { + var selector, parentMatching = true; + while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { + if (parentMatching) { + selector = selectors.pop(); + } + element = element.parentNode; + parentMatching = selectorMatches(element, selector); + } + return selectors.length === 0; + } + + /** + * @private + */ + function selectorMatches(element, selector) { + var nodeName = element.nodeName, + classNames = element.getAttribute('class'), + id = element.getAttribute('id'), matcher, i; + // i check if a selector matches slicing away part from it. + // if i get empty string i should match + matcher = new RegExp('^' + nodeName, 'i'); + selector = selector.replace(matcher, ''); + if (id && selector.length) { + matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + if (classNames && selector.length) { + classNames = classNames.split(' '); + for (i = classNames.length; i--;) { + matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + } + return selector.length === 0; + } + + /** + * @private + * to support IE8 missing getElementById on SVGdocument and on node xmlDOM + */ + function elementById(doc, id) { + var el; + doc.getElementById && (el = doc.getElementById(id)); + if (el) { + return el; + } + var node, i, len, nodelist = doc.getElementsByTagName('*'); + for (i = 0, len = nodelist.length; i < len; i++) { + node = nodelist[i]; + if (id === node.getAttribute('id')) { + return node; + } + } + } + + /** + * @private + */ + function parseUseDirectives(doc) { + var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; + while (nodelist.length && i < nodelist.length) { + var el = nodelist[i], + xlinkAttribute = el.getAttribute('xlink:href') || el.getAttribute('href'); + + if (xlinkAttribute === null) { + return; + } + + var xlink = xlinkAttribute.substr(1), + x = el.getAttribute('x') || 0, + y = el.getAttribute('y') || 0, + el2 = elementById(doc, xlink).cloneNode(true), + currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', + parentNode, + oldLength = nodelist.length, attr, + j, + attrs, + len, + namespace = fabric.svgNS; + + applyViewboxTransform(el2); + if (/^svg$/i.test(el2.nodeName)) { + var el3 = el2.ownerDocument.createElementNS(namespace, 'g'); + for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + el3.setAttributeNS(namespace, attr.nodeName, attr.nodeValue); + } + // el2.firstChild != null + while (el2.firstChild) { + el3.appendChild(el2.firstChild); + } + el2 = el3; + } + + for (j = 0, attrs = el.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + if (attr.nodeName === 'x' || attr.nodeName === 'y' || + attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { + continue; + } + + if (attr.nodeName === 'transform') { + currentTrans = attr.nodeValue + ' ' + currentTrans; + } + else { + el2.setAttribute(attr.nodeName, attr.nodeValue); + } + } + + el2.setAttribute('transform', currentTrans); + el2.setAttribute('instantiated_by_use', '1'); + el2.removeAttribute('id'); + parentNode = el.parentNode; + parentNode.replaceChild(el2, el); + // some browsers do not shorten nodelist after replaceChild (IE8) + if (nodelist.length === oldLength) { + i++; + } + } + } + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // matches, e.g.: +14.56e-12, etc. + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*' + + '$' + ); + + /** + * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements + */ + function applyViewboxTransform(element) { + if (!fabric.svgViewBoxElementsRegEx.test(element.nodeName)) { + return {}; + } + var viewBoxAttr = element.getAttribute('viewBox'), + scaleX = 1, + scaleY = 1, + minX = 0, + minY = 0, + viewBoxWidth, viewBoxHeight, matrix, el, + widthAttr = element.getAttribute('width'), + heightAttr = element.getAttribute('height'), + x = element.getAttribute('x') || 0, + y = element.getAttribute('y') || 0, + preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', + missingViewBox = (!viewBoxAttr || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), + missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), + toBeParsed = missingViewBox && missingDimAttr, + parsedDim = { }, translateMatrix = '', widthDiff = 0, heightDiff = 0; + + parsedDim.width = 0; + parsedDim.height = 0; + parsedDim.toBeParsed = toBeParsed; + + if (missingViewBox) { + if (((x || y) && element.parentNode && element.parentNode.nodeName !== '#document')) { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + matrix = (element.getAttribute('transform') || '') + translateMatrix; + element.setAttribute('transform', matrix); + element.removeAttribute('x'); + element.removeAttribute('y'); + } + } + + if (toBeParsed) { + return parsedDim; + } + + if (missingViewBox) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + // set a transform for elements that have x y and are inner(only) SVGs + return parsedDim; + } + minX = -parseFloat(viewBoxAttr[1]); + minY = -parseFloat(viewBoxAttr[2]); + viewBoxWidth = parseFloat(viewBoxAttr[3]); + viewBoxHeight = parseFloat(viewBoxAttr[4]); + parsedDim.minX = minX; + parsedDim.minY = minY; + parsedDim.viewBoxWidth = viewBoxWidth; + parsedDim.viewBoxHeight = viewBoxHeight; + if (!missingDimAttr) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + scaleX = parsedDim.width / viewBoxWidth; + scaleY = parsedDim.height / viewBoxHeight; + } + else { + parsedDim.width = viewBoxWidth; + parsedDim.height = viewBoxHeight; + } + + // default is to preserve aspect ratio + preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); + if (preserveAspectRatio.alignX !== 'none') { + //translate all container for the effect of Mid, Min, Max + if (preserveAspectRatio.meetOrSlice === 'meet') { + scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); + // calculate additional translation to move the viewbox + } + if (preserveAspectRatio.meetOrSlice === 'slice') { + scaleY = scaleX = (scaleX > scaleY ? scaleX : scaleY); + // calculate additional translation to move the viewbox + } + widthDiff = parsedDim.width - viewBoxWidth * scaleX; + heightDiff = parsedDim.height - viewBoxHeight * scaleX; + if (preserveAspectRatio.alignX === 'Mid') { + widthDiff /= 2; + } + if (preserveAspectRatio.alignY === 'Mid') { + heightDiff /= 2; + } + if (preserveAspectRatio.alignX === 'Min') { + widthDiff = 0; + } + if (preserveAspectRatio.alignY === 'Min') { + heightDiff = 0; + } + } + + if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { + return parsedDim; + } + if ((x || y) && element.parentNode.nodeName !== '#document') { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + } + + matrix = translateMatrix + ' matrix(' + scaleX + + ' 0' + + ' 0 ' + + scaleY + ' ' + + (minX * scaleX + widthDiff) + ' ' + + (minY * scaleY + heightDiff) + ') '; + // seems unused. + // parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); + if (element.nodeName === 'svg') { + el = element.ownerDocument.createElementNS(fabric.svgNS, 'g'); + // element.firstChild != null + while (element.firstChild) { + el.appendChild(element.firstChild); + } + element.appendChild(el); + } + else { + el = element; + el.removeAttribute('x'); + el.removeAttribute('y'); + matrix = el.getAttribute('transform') + matrix; + } + el.setAttribute('transform', matrix); + return parsedDim; + } + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) + && !element.getAttribute('instantiated_by_use')) { + return true; + } + } + return false; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; + * It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [parsingOptions] options for parsing document + * @param {String} [parsingOptions.crossOrigin] crossOrigin settings + */ + fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { + if (!doc) { + return; + } + + parseUseDirectives(doc); + + var svgUid = fabric.Object.__uid++, i, len, + options = applyViewboxTransform(doc), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; + options.svgUid = svgUid; + + if (descendants.length === 0 && fabric.isLikelyNode) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes('//*[name(.)!="svg"]'); + var arr = []; + for (i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + applyViewboxTransform(el); + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')) && + !hasAncestorWithNodeName(el, fabric.svgInvalidAncestorsRegEx); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + if (!elements || (elements && !elements.length)) { + callback && callback([], {}); + return; + } + var clipPaths = { }; + descendants.filter(function(el) { + return el.nodeName.replace('svg:', '') === 'clipPath'; + }).forEach(function(el) { + var id = el.getAttribute('id'); + clipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) { + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); + }); + }); + fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + fabric.clipPaths[svgUid] = clipPaths; + // Precedence of rules: style > class > attribute + fabric.parseElements(elements, function(instances, elements) { + if (callback) { + callback(instances, options, elements, descendants); + delete fabric.gradientDefs[svgUid]; + delete fabric.cssRules[svgUid]; + delete fabric.clipPaths[svgUid]; + } + }, clone(options), reviver, parsingOptions); + }; + + function recursivelyParseGradientsXlink(doc, gradient) { + var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], + xlinkAttr = 'xlink:href', + xLink = gradient.getAttribute(xlinkAttr).substr(1), + referencedGradient = elementById(doc, xLink); + if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { + recursivelyParseGradientsXlink(doc, referencedGradient); + } + gradientsAttrs.forEach(function(attr) { + if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { + gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); + } + }); + if (!gradient.children.length) { + var referenceClone = referencedGradient.cloneNode(true); + while (referenceClone.firstChild) { + gradient.appendChild(referenceClone.firstChild); + } + } + gradient.removeAttribute(xlinkAttr); + } + + var reFontDeclaration = new RegExp( + '(normal|italic)?\\s*(normal|small-caps)?\\s*' + + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + + fabric.reNum + + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); + + extend(fabric, { + /** + * Parses a short font declaration, building adding its properties to a style object + * @static + * @function + * @memberOf fabric + * @param {String} value font declaration + * @param {Object} oStyle definition + */ + parseFontDeclaration: function(value, oStyle) { + var match = value.match(reFontDeclaration); + + if (!match) { + return; + } + var fontStyle = match[1], + // font variant is not used + // fontVariant = match[2], + fontWeight = match[3], + fontSize = match[4], + lineHeight = match[5], + fontFamily = match[6]; + + if (fontStyle) { + oStyle.fontStyle = fontStyle; + } + if (fontWeight) { + oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); + } + if (fontSize) { + oStyle.fontSize = parseUnit(fontSize); + } + if (fontFamily) { + oStyle.fontFamily = fontFamily; + } + if (lineHeight) { + oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var tagArray = [ + 'linearGradient', + 'radialGradient', + 'svg:linearGradient', + 'svg:radialGradient'], + elList = _getMultipleNodes(doc, tagArray), + el, j = 0, gradientDefs = { }; + j = elList.length; + while (j--) { + el = elList[j]; + if (el.getAttribute('xlink:href')) { + recursivelyParseGradientsXlink(doc, el); + } + gradientDefs[el.getAttribute('id')] = el; + } + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes, svgUid) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }, + fontSize, parentFontSize; + + if (typeof svgUid === 'undefined') { + svgUid = element.getAttribute('svgUid'); + } + // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards + if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { // eslint-disable-line + memo[attr] = value; + } + return memo; + }, { }); + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + var cssAttrs = extend( + getGlobalStylesForElement(element, svgUid), + fabric.parseStyleAttribute(element) + ); + ownAttributes = extend( + ownAttributes, + cssAttrs + ); + if (cssAttrs[cPath]) { + element.setAttribute(cPath, cssAttrs[cPath]); + } + fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; + if (ownAttributes[fSize]) { + // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. + ownAttributes[fSize] = fontSize = parseUnit(ownAttributes[fSize], parentFontSize); + } + + var normalizedAttr, normalizedValue, normalizedStyle = {}; + for (var attr in ownAttributes) { + normalizedAttr = normalizeAttr(attr); + normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); + normalizedStyle[normalizedAttr] = normalizedValue; + } + if (normalizedStyle && normalizedStyle.font) { + fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); + } + var mergedAttrs = extend(parentAttributes, normalizedStyle); + return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver, parsingOptions) { + new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) { + return oStyle; + } + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param {String} points points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) { + return null; + } + + // replace commas with whitespace and remove bookending whitespace + points = points.replace(/,/g, ' ').trim(); + + points = points.split(/\s+/); + var parsedPoints = [], i, len; + + for (i = 0, len = points.length; i < len; i += 2) { + parsedPoints.push({ + x: parseFloat(points[i]), + y: parseFloat(points[i + 1]) + }); + } + + // odd number of points is an error + // if (parsedPoints.length % 2 !== 0) { + // return null; + // } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), i, len, + allRules = { }, rules; + + // very crude parsing of style contents + for (i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[i].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + if (styleContents.trim() === '') { + continue; + } + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + // eslint-disable-next-line no-loop-func + rules.forEach(function(rule) { + + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), + ruleObj = { }, declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + for (i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + ruleObj[property] = value; + } + rule = match[1]; + rule.split(',').forEach(function(_rule) { + _rule = _rule.replace(/^svg/i, '').trim(); + if (_rule === '') { + return; + } + if (allRules[_rule]) { + fabric.util.object.extend(allRules[_rule], ruleObj); + } + else { + allRules[_rule] = fabric.util.object.clone(ruleObj); + } + }); + }); + } + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. + * Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberOf fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromURL: function(url, callback, reviver, options) { + + url = url.replace(/^\n\s*/, '').trim(); + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml || !xml.documentElement) { + callback && callback(null); + return false; + } + + fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { + callback && callback(results, _options, elements, allElements); + }, reviver, options); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberOf fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromString: function(string, callback, reviver, options) { + var parser = new fabric.window.DOMParser(), + doc = parser.parseFromString(string.trim(), 'text/xml'); + fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { + callback(results, _options, elements, allElements); + }, reviver, options); + } + }); + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions, doc) { + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + this.svgUid = (options && options.svgUid) || 0; + this.parsingOptions = parsingOptions; + this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g; + this.doc = doc; +}; + +(function(proto) { + proto.parse = function() { + this.instances = new Array(this.elements.length); + this.numElements = this.elements.length; + this.createObjects(); + }; + + proto.createObjects = function() { + var _this = this; + this.elements.forEach(function(element, i) { + element.setAttribute('svgUid', _this.svgUid); + _this.createObject(element, i); + }); + }; + + proto.findTag = function(el) { + return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; + }; + + proto.createObject = function(el, index) { + var klass = this.findTag(el); + if (klass && klass.fromElement) { + try { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + catch (err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }; + + proto.createCallback = function(index, el) { + var _this = this; + return function(obj) { + var _options; + _this.resolveGradient(obj, el, 'fill'); + _this.resolveGradient(obj, el, 'stroke'); + if (obj instanceof fabric.Image && obj._originalElement) { + _options = obj.parsePreserveAspectRatioAttribute(el); + } + obj._removeTransformMatrix(_options); + _this.resolveClipPath(obj, el); + _this.reviver && _this.reviver(el, obj); + _this.instances[index] = obj; + _this.checkIfDone(); + }; + }; + + proto.extractPropertyDefinition = function(obj, property, storage) { + var value = obj[property], regex = this.regexUrl; + if (!regex.test(value)) { + return; + } + regex.lastIndex = 0; + var id = regex.exec(value)[1]; + regex.lastIndex = 0; + return fabric[storage][this.svgUid][id]; + }; + + proto.resolveGradient = function(obj, el, property) { + var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); + if (gradientDef) { + var opacityAttr = el.getAttribute(property + '-opacity'); + var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); + obj.set(property, gradient); + } + }; + + proto.createClipPathCallback = function(obj, container) { + return function(_newObj) { + _newObj._removeTransformMatrix(); + _newObj.fillRule = _newObj.clipRule; + container.push(_newObj); + }; + }; + + proto.resolveClipPath = function(obj, usingElement) { + var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'), + element, klass, objTransformInv, container, gTransform, options; + if (clipPath) { + container = []; + objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix()); + // move the clipPath tag as sibling to the real element that is using it + var clipPathTag = clipPath[0].parentNode; + var clipPathOwner = usingElement; + while (clipPathOwner.parentNode && clipPathOwner.getAttribute('clip-path') !== obj.clipPath) { + clipPathOwner = clipPathOwner.parentNode; + } + clipPathOwner.parentNode.appendChild(clipPathTag); + for (var i = 0; i < clipPath.length; i++) { + element = clipPath[i]; + klass = this.findTag(element); + klass.fromElement( + element, + this.createClipPathCallback(obj, container), + this.options + ); + } + if (container.length === 1) { + clipPath = container[0]; + } + else { + clipPath = new fabric.Group(container); + } + gTransform = fabric.util.multiplyTransformMatrices( + objTransformInv, + clipPath.calcTransformMatrix() + ); + if (clipPath.clipPath) { + this.resolveClipPath(clipPath, clipPathOwner); + } + var options = fabric.util.qrDecompose(gTransform); + clipPath.flipX = false; + clipPath.flipY = false; + clipPath.set('scaleX', options.scaleX); + clipPath.set('scaleY', options.scaleY); + clipPath.angle = options.angle; + clipPath.skewX = options.skewX; + clipPath.skewY = 0; + clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center'); + obj.clipPath = clipPath; + } + else { + // if clip-path does not resolve to any element, delete the property. + delete obj.clipPath; + } + }; + + proto.checkIfDone = function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + // eslint-disable-next-line no-eq-null, eqeqeq + return el != null; + }); + this.callback(this.instances, this.elements); + } + }; +})(fabric.ElementsParser.prototype); + + +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Point) { + fabric.warn('fabric.Point is already defined'); + return; + } + + fabric.Point = Point; + + /** + * Point class + * @class fabric.Point + * @memberOf fabric + * @constructor + * @param {Number} x + * @param {Number} y + * @return {fabric.Point} thisArg + */ + function Point(x, y) { + this.x = x; + this.y = y; + } + + Point.prototype = /** @lends fabric.Point.prototype */ { + + type: 'point', + + constructor: Point, + + /** + * Adds another point to this one and returns another one + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * Adds another point to this one + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * Adds value to this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * Adds value to this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * Subtracts another point from this point and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * Subtracts another point from this point + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + subtractEquals: function (that) { + this.x -= that.x; + this.y -= that.y; + return this; + }, + + /** + * Subtracts value from this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + scalarSubtract: function (scalar) { + return new Point(this.x - scalar, this.y - scalar); + }, + + /** + * Subtracts value from this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarSubtractEquals: function (scalar) { + this.x -= scalar; + this.y -= scalar; + return this; + }, + + /** + * Multiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + multiply: function (scalar) { + return new Point(this.x * scalar, this.y * scalar); + }, + + /** + * Multiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + multiplyEquals: function (scalar) { + this.x *= scalar; + this.y *= scalar; + return this; + }, + + /** + * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + divide: function (scalar) { + return new Point(this.x / scalar, this.y / scalar); + }, + + /** + * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + divideEquals: function (scalar) { + this.x /= scalar; + this.y /= scalar; + return this; + }, + + /** + * Returns true if this point is equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + eq: function (that) { + return (this.x === that.x && this.y === that.y); + }, + + /** + * Returns true if this point is less than another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lt: function (that) { + return (this.x < that.x && this.y < that.y); + }, + + /** + * Returns true if this point is less than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lte: function (that) { + return (this.x <= that.x && this.y <= that.y); + }, + + /** + + * Returns true if this point is greater another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gt: function (that) { + return (this.x > that.x && this.y > that.y); + }, + + /** + * Returns true if this point is greater than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gte: function (that) { + return (this.x >= that.x && this.y >= that.y); + }, + + /** + * Returns new point which is the result of linear interpolation with this one and another one + * @param {fabric.Point} that + * @param {Number} t , position of interpolation, between 0 and 1 default 0.5 + * @return {fabric.Point} + */ + lerp: function (that, t) { + if (typeof t === 'undefined') { + t = 0.5; + } + t = Math.max(Math.min(1, t), 0); + return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); + }, + + /** + * Returns distance from this point and another one + * @param {fabric.Point} that + * @return {Number} + */ + distanceFrom: function (that) { + var dx = this.x - that.x, + dy = this.y - that.y; + return Math.sqrt(dx * dx + dy * dy); + }, + + /** + * Returns the point between this point and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + midPointFrom: function (that) { + return this.lerp(that); + }, + + /** + * Returns a new point which is the min of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + min: function (that) { + return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); + }, + + /** + * Returns a new point which is the max of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + max: function (that) { + return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); + }, + + /** + * Returns string representation of this point + * @return {String} + */ + toString: function () { + return this.x + ',' + this.y; + }, + + /** + * Sets x/y of this point + * @param {Number} x + * @param {Number} y + * @chainable + */ + setXY: function (x, y) { + this.x = x; + this.y = y; + return this; + }, + + /** + * Sets x of this point + * @param {Number} x + * @chainable + */ + setX: function (x) { + this.x = x; + return this; + }, + + /** + * Sets y of this point + * @param {Number} y + * @chainable + */ + setY: function (y) { + this.y = y; + return this; + }, + + /** + * Sets x/y of this point from another point + * @param {fabric.Point} that + * @chainable + */ + setFromPoint: function (that) { + this.x = that.x; + this.y = that.y; + return this; + }, + + /** + * Swaps x/y of this point and another point + * @param {fabric.Point} that + */ + swap: function (that) { + var x = this.x, + y = this.y; + this.x = that.x; + this.y = that.y; + that.x = x; + that.y = y; + }, + + /** + * return a cloned instance of the point + * @return {fabric.Point} + */ + clone: function () { + return new Point(this.x, this.y); + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + constructor: Intersection, + + /** + * Appends a point to intersection + * @param {fabric.Point} point + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoint: function (point) { + this.points.push(point); + return this; + }, + + /** + * Appends points to intersection + * @param {Array} points + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + return this; + } + }; + + /** + * Checks if one line intersects another + * TODO: rename in intersectSegmentSegment + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (uB !== 0) { + var ua = uaT / uB, + ub = ubT / uB; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection('Intersection'); + result.appendPoint(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (uaT === 0 || ubT === 0) { + result = new Intersection('Coincident'); + } + else { + result = new Intersection('Parallel'); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * TODO: rename in intersectSegmentPolygon + * fix detection of coincident + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1, a2, points) { + var result = new Intersection(), + length = points.length, + b1, b2, inter, i; + + for (i = 0; i < length; i++) { + b1 = points[i]; + b2 = points[(i + 1) % length]; + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length, i; + + for (i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i + 1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {fabric.Point} r1 + * @param {fabric.Point} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * Color class + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class fabric.Color + * @param {String} color optional in hex or rgb(a) or hsl format or from known color list + * @return {fabric.Color} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} + */ + function Color(color) { + if (!color) { + this.setSource([0, 0, 0, 1]); + } + else { + this._tryParsingColor(color); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @lends fabric.Color.prototype */ { + + /** + * @private + * @param {String|Array} color Color value to parse + */ + _tryParsingColor: function(color) { + var source; + + if (color in Color.colorNameMap) { + color = Color.colorNameMap[color]; + } + + if (color === 'transparent') { + source = [255, 255, 255, 0]; + } + + if (!source) { + source = Color.sourceFromHex(color); + } + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (!source) { + //if color is not recognize let's make black as canvas does + source = [0, 0, 0, 1]; + } + if (source) { + this.setSource(source); + } + }, + + /** + * Adapted from https://github.com/mjijackson + * @private + * @param {Number} r Red color value + * @param {Number} g Green color value + * @param {Number} b Blue color value + * @return {Array} Hsl color + */ + _rgbToHsl: function(r, g, b) { + r /= 255; g /= 255; b /= 255; + + var h, s, l, + max = fabric.util.array.max([r, g, b]), + min = fabric.util.array.min([r, g, b]); + + l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [ + Math.round(h * 360), + Math.round(s * 100), + Math.round(l * 100) + ]; + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color representation in RGB format + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color representation in RGBA format + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color representation in HSL format + * @return {String} ex: hsl(0-360,0%-100%,0%-100%) + */ + toHsl: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; + }, + + /** + * Returns color representation in HSLA format + * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) + */ + toHsla: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; + }, + + /** + * Returns color representation in HEX format + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(), r, g, b; + + r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Returns color representation in HEXA format + * @return {String} ex: FF5555CC + */ + toHexa: function() { + var source = this.getSource(), a; + + a = Math.round(source[3] * 255); + a = a.toString(16); + a = (a.length === 1) ? ('0' + a) : a; + + return this.toHex() + a.toUpperCase(); + }, + + /** + * Gets value of alpha channel for this color + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @param {Number} alpha Alpha value 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @return {fabric.Color} thisArg + */ + toGrayscale: function() { + var source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Transforms color to its black and white representation + * @param {Number} threshold + * @return {fabric.Color} thisArg + */ + toBlackWhite: function(threshold) { + var source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3]; + + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Overlays color with another color + * @param {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); + } + + var result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(), i; + + for (i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); + } + + result[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) + * @static + * @field + * @memberOf fabric.Color + */ + // eslint-disable-next-line max-len + fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HEX format (ex: #FF5544CC, #FF5555, 010155, aff) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; + + /** + * Map of the 148 color names with HEX code + * @static + * @field + * @memberOf fabric.Color + * @see: https://www.w3.org/TR/css3-color/#svg-color + */ + fabric.Color.colorNameMap = { + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aqua: '#00FFFF', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + black: '#000000', + blanchedalmond: '#FFEBCD', + blue: '#0000FF', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + cyan: '#00FFFF', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgrey: '#A9A9A9', + darkgreen: '#006400', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + fuchsia: '#FF00FF', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + gray: '#808080', + grey: '#808080', + green: '#008000', + greenyellow: '#ADFF2F', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgray: '#D3D3D3', + lightgrey: '#D3D3D3', + lightgreen: '#90EE90', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + lime: '#00FF00', + limegreen: '#32CD32', + linen: '#FAF0E6', + magenta: '#FF00FF', + maroon: '#800000', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + navy: '#000080', + oldlace: '#FDF5E6', + olive: '#808000', + olivedrab: '#6B8E23', + orange: '#FFA500', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + purple: '#800080', + rebeccapurple: '#663399', + red: '#FF0000', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + silver: '#C0C0C0', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + teal: '#008080', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + white: '#FFFFFF', + whitesmoke: '#F5F5F5', + yellow: '#FFFF00', + yellowgreen: '#9ACD32' + }; + + /** + * @private + * @param {Number} p + * @param {Number} q + * @param {Number} t + * @return {Number} + */ + function hue2rgb(p, q, t) { + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } + return p; + } + + /** + * Returns new color object, when given a color in RGB format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) + * @return {Array} source + */ + fabric.Color.sourceFromRgb = function(color) { + var match = color.match(Color.reRGBa); + if (match) { + var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), + g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), + b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); + + return [ + parseInt(r, 10), + parseInt(g, 10), + parseInt(b, 10), + match[4] ? parseFloat(match[4]) : 1 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HSL format + * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) + * @memberOf fabric.Color + * @return {fabric.Color} + */ + fabric.Color.fromHsl = function(color) { + return Color.fromSource(Color.sourceFromHsl(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. + * Adapted from https://github.com/mjijackson + * @memberOf fabric.Color + * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) + * @return {Array} source + * @see http://http://www.w3.org/TR/css3-color/#hsl-color + */ + fabric.Color.sourceFromHsl = function(color) { + var match = color.match(Color.reHSLa); + if (!match) { + return; + } + + var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, + s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), + l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), + r, g, b; + + if (s === 0) { + r = g = b = l; + } + else { + var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, + p = l * 2 - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255), + match[4] ? parseFloat(match[4]) : 1 + ]; + }; + + /** + * Returns new color object, when given a color in HSLA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromHsla = Color.fromHsl; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color Color value ex: FF5555 + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color ex: FF5555 or FF5544CC (RGBa) + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3 || value.length === 4), + isRGBa = (value.length === 8 || value.length === 4), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), + a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + parseFloat((parseInt(a, 16) / 255).toFixed(2)) + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @memberOf fabric.Color + * @param {Array} source + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + scaleMap = ['e', 'se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'], + skewMap = ['ns', 'nesw', 'ew', 'nwse'], + controls = {}, + LEFT = 'left', TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', CENTER = 'center', + opposite = { + top: BOTTOM, + bottom: TOP, + left: RIGHT, + right: LEFT, + center: CENTER, + }, radiansToDegrees = fabric.util.radiansToDegrees, + sign = (Math.sign || function(x) { return ((x > 0) - (x < 0)) || +x; }); + + /** + * Combine control position and object angle to find the control direction compared + * to the object center. + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + * @param {fabric.Control} control the control class + * @return {Number} 0 - 7 a quadrant number + */ + function findCornerQuadrant(fabricObject, control) { + var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + return Math.round((cornerAngle % 360) / 45); + } + + function fireEvent(eventName, options) { + var target = options.transform.target, + canvas = target.canvas, + canvasOptions = fabric.util.object.clone(options); + canvasOptions.target = target; + canvas && canvas.fire('object:' + eventName, canvasOptions); + target.fire(eventName, options); + } + + /** + * Inspect event and fabricObject properties to understand if the scaling action + * @param {Event} eventData from the user action + * @param {fabric.Object} fabricObject the fabric object about to scale + * @return {Boolean} true if scale is proportional + */ + function scaleIsProportional(eventData, fabricObject) { + var canvas = fabricObject.canvas, uniScaleKey = canvas.uniScaleKey, + uniformIsToggled = eventData[uniScaleKey]; + return (canvas.uniformScaling && !uniformIsToggled) || + (!canvas.uniformScaling && uniformIsToggled); + } + + /** + * Checks if transform is centered + * @param {Object} transform transform data + * @return {Boolean} true if transform is centered + */ + function isTransformCentered(transform) { + return transform.originX === CENTER && transform.originY === CENTER; + } + + /** + * Inspect fabricObject to understand if the current scaling action is allowed + * @param {fabric.Object} fabricObject the fabric object about to scale + * @param {String} by 'x' or 'y' or '' + * @param {Boolean} scaleProportionally true if we are trying to scale proportionally + * @return {Boolean} true if scaling is not allowed at current conditions + */ + function scalingIsForbidden(fabricObject, by, scaleProportionally) { + var lockX = fabricObject.lockScalingX, lockY = fabricObject.lockScalingY; + if (lockX && lockY) { + return true; + } + if (!by && (lockX || lockY) && scaleProportionally) { + return true; + } + if (lockX && by === 'x') { + return true; + } + if (lockY && by === 'y') { + return true; + } + return false; + } + + /** + * return the correct cursor style for the scale action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed', + scaleProportionally = scaleIsProportional(eventData, fabricObject), + by = ''; + if (control.x !== 0 && control.y === 0) { + by = 'x'; + } + else if (control.x === 0 && control.y !== 0) { + by = 'y'; + } + if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control); + return scaleMap[n] + '-resize'; + } + + /** + * return the correct cursor style for the skew action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function skewCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed'; + if (control.x !== 0 && fabricObject.lockSkewingY) { + return notAllowed; + } + if (control.y !== 0 && fabricObject.lockSkewingX) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control) % 4; + return skewMap[n] + '-resize'; + } + + /** + * Combine skew and scale style handlers to cover fabric standard use case + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleSkewCursorStyleHandler(eventData, control, fabricObject) { + if (eventData[fabricObject.canvas.altActionKey]) { + return controls.skewCursorStyleHandler(eventData, control, fabricObject); + } + return controls.scaleCursorStyleHandler(eventData, control, fabricObject); + } + + /** + * Inspect event, control and fabricObject to return the correct action name + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} an action name + */ + function scaleOrSkewActionName(eventData, control, fabricObject) { + var isAlternative = eventData[fabricObject.canvas.altActionKey]; + if (control.x === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewX' : 'scaleY'; + } + if (control.y === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewY' : 'scaleX'; + } + } + + /** + * Find the correct style for the control that is used for rotation. + * this function is very simple and it just take care of not-allowed or standard cursor + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function rotationStyleHandler(eventData, control, fabricObject) { + if (fabricObject.lockRotation) { + return 'not-allowed'; + } + return control.cursorStyle; + } + + function commonEventInfo(eventData, transform, x, y) { + return { + e: eventData, + transform: transform, + pointer: { + x: x, + y: y, + } + }; + } + + /** + * Wrap an action handler with saving/restoring object position on the transform. + * this is the code that permits to objects to keep their position while transforming. + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFixedAnchor(actionHandler) { + return function(eventData, transform, x, y) { + var target = transform.target, centerPoint = target.getCenterPoint(), + constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), + actionPerformed = actionHandler(eventData, transform, x, y); + target.setPositionByOrigin(constraint, transform.originX, transform.originY); + return actionPerformed; + }; + } + + /** + * Wrap an action handler with firing an event if the action is performed + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFireEvent(eventName, actionHandler) { + return function(eventData, transform, x, y) { + var actionPerformed = actionHandler(eventData, transform, x, y); + if (actionPerformed) { + fireEvent(eventName, commonEventInfo(eventData, transform, x, y)); + } + return actionPerformed; + }; + } + + /** + * Transforms a point described by x and y in a distance from the top left corner of the object + * bounding box. + * @param {Object} transform + * @param {String} originX + * @param {String} originY + * @param {number} x + * @param {number} y + * @return {Fabric.Point} the normalized point + */ + function getLocalPoint(transform, originX, originY, x, y) { + var target = transform.target, + control = target.controls[transform.corner], + zoom = target.canvas.getZoom(), + padding = target.padding / zoom, + localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); + if (localPoint.x >= padding) { + localPoint.x -= padding; + } + if (localPoint.x <= -padding) { + localPoint.x += padding; + } + if (localPoint.y >= padding) { + localPoint.y -= padding; + } + if (localPoint.y <= padding) { + localPoint.y += padding; + } + localPoint.x -= control.offsetX; + localPoint.y -= control.offsetY; + return localPoint; + } + + /** + * Detect if the fabric object is flipped on one side. + * @param {fabric.Object} target + * @return {Boolean} true if one flip, but not two. + */ + function targetHasOneFlip(target) { + return target.flipX !== target.flipY; + } + + /** + * Utility function to compensate the scale factor when skew is applied on both axes + * @private + */ + function compensateScaleForSkew(target, oppositeSkew, scaleToCompensate, axis, reference) { + if (target[oppositeSkew] !== 0) { + var newDim = target._getTransformedDimensions()[axis]; + var newValue = reference / newDim * target[scaleToCompensate]; + target.set(scaleToCompensate, newValue); + } + } + + /** + * Action handler for skewing on the X axis + * @private + */ + function skewObjectX(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(0, target.skewY), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.x * 2) - dimNoSkew.x, + currentSkew = target.skewX, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleX), (dimNoSkew.y / target.scaleY)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().y; + target.set('skewX', newSkew); + compensateScaleForSkew(target, 'skewY', 'scaleY', 'y', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Action handler for skewing on the Y axis + * @private + */ + function skewObjectY(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(target.skewX, 0), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.y * 2) - dimNoSkew.y, + currentSkew = target.skewY, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleY), (dimNoSkew.x / target.scaleX)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().x; + target.set('skewY', newSkew); + compensateScaleForSkew(target, 'skewX', 'scaleX', 'x', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerX(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewX > 0 and originY bottom we anchor on right + // if skewX > 0 and originY top we anchor on left + // if skewX < 0 and originY bottom we anchor on left + // if skewX < 0 and originY top we anchor on right + // if skewX is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewX, originX, originY = transform.originY; + if (target.lockSkewingX) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.x > 0) { + // we are pulling right, anchor left; + originX = LEFT; + } + else { + // we are pulling right, anchor right + originX = RIGHT; + } + } + else { + if (currentSkew > 0) { + originX = originY === TOP ? LEFT : RIGHT; + } + if (currentSkew < 0) { + originX = originY === TOP ? RIGHT : LEFT; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originX = originX === LEFT ? RIGHT : LEFT; + } + } + + // once we have the origin, we find the anchor point + transform.originX = originX; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectX)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerY(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewY > 0 and originX left we anchor on top + // if skewY > 0 and originX right we anchor on bottom + // if skewY < 0 and originX left we anchor on bottom + // if skewY < 0 and originX right we anchor on top + // if skewY is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewY, originY, originX = transform.originX; + if (target.lockSkewingY) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.y > 0) { + // we are pulling down, anchor up; + originY = TOP; + } + else { + // we are pulling up, anchor down + originY = BOTTOM; + } + } + else { + if (currentSkew > 0) { + originY = originX === LEFT ? TOP : BOTTOM; + } + if (currentSkew < 0) { + originY = originX === LEFT ? BOTTOM : TOP; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originY = originY === TOP ? BOTTOM : TOP; + } + } + + // once we have the origin, we find the anchor point + transform.originY = originY; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectY)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Action handler for rotation and snapping, without anchor point. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + * @private + */ + function rotationWithSnapping(eventData, transform, x, y) { + var t = transform, + target = t.target, + pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + + if (target.lockRotation) { + return false; + } + + var lastAngle = Math.atan2(t.ey - pivotPoint.y, t.ex - pivotPoint.x), + curAngle = Math.atan2(y - pivotPoint.y, x - pivotPoint.x), + angle = radiansToDegrees(curAngle - lastAngle + t.theta), + hasRotated = true; + + if (target.snapAngle > 0) { + var snapAngle = target.snapAngle, + snapThreshold = target.snapThreshold || snapAngle, + rightAngleLocked = Math.ceil(angle / snapAngle) * snapAngle, + leftAngleLocked = Math.floor(angle / snapAngle) * snapAngle; + + if (Math.abs(angle - leftAngleLocked) < snapThreshold) { + angle = leftAngleLocked; + } + else if (Math.abs(angle - rightAngleLocked) < snapThreshold) { + angle = rightAngleLocked; + } + } + + // normalize angle to positive value + if (angle < 0) { + angle = 360 + angle; + } + angle %= 360; + + hasRotated = target.angle !== angle; + target.angle = angle; + return hasRotated; + } + + /** + * Basic scaling logic, reused with different constrain for scaling X,Y, freely or equally. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @param {Object} options additional information for scaling + * @param {String} options.by 'x', 'y', 'equally' or '' to indicate type of scaling + * @return {Boolean} true if some change happened + * @private + */ + function scaleObject(eventData, transform, x, y, options) { + options = options || {}; + var target = transform.target, + lockScalingX = target.lockScalingX, lockScalingY = target.lockScalingY, + by = options.by, newPoint, scaleX, scaleY, dim, + scaleProportionally = scaleIsProportional(eventData, target), + forbidScaling = scalingIsForbidden(target, by, scaleProportionally), + signX, signY, gestureScale = transform.gestureScale; + + if (forbidScaling) { + return false; + } + if (gestureScale) { + scaleX = transform.scaleX * gestureScale; + scaleY = transform.scaleY * gestureScale; + } + else { + newPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y); + // use of sign: We use sign to detect change of direction of an action. sign usually change when + // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling + // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily + // cross many time the origin point and flip the object. so we need a way to filter out the noise. + // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. + signX = by !== 'y' ? sign(newPoint.x) : 1; + signY = by !== 'x' ? sign(newPoint.y) : 1; + if (!transform.signX) { + transform.signX = signX; + } + if (!transform.signY) { + transform.signY = signY; + } + + if (target.lockScalingFlip && + (transform.signX !== signX || transform.signY !== signY) + ) { + return false; + } + + dim = target._getTransformedDimensions(); + // missing detection of flip and logic to switch the origin + if (scaleProportionally && !by) { + // uniform scaling + var distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), + original = transform.original, + originalDistance = Math.abs(dim.x * original.scaleX / target.scaleX) + + Math.abs(dim.y * original.scaleY / target.scaleY), + scale = distance / originalDistance; + scaleX = original.scaleX * scale; + scaleY = original.scaleY * scale; + } + else { + scaleX = Math.abs(newPoint.x * target.scaleX / dim.x); + scaleY = Math.abs(newPoint.y * target.scaleY / dim.y); + } + // if we are scaling by center, we need to double the scale + if (isTransformCentered(transform)) { + scaleX *= 2; + scaleY *= 2; + } + if (transform.signX !== signX && by !== 'y') { + transform.originX = opposite[transform.originX]; + scaleX *= -1; + transform.signX = signX; + } + if (transform.signY !== signY && by !== 'x') { + transform.originY = opposite[transform.originY]; + scaleY *= -1; + transform.signY = signY; + } + } + // minScale is taken are in the setter. + var oldScaleX = target.scaleX, oldScaleY = target.scaleY; + if (!by) { + !lockScalingX && target.set('scaleX', scaleX); + !lockScalingY && target.set('scaleY', scaleY); + } + else { + // forbidden cases already handled on top here. + by === 'x' && target.set('scaleX', scaleX); + by === 'y' && target.set('scaleY', scaleY); + } + return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; + } + + /** + * Generic scaling logic, to scale from corners either equally or freely. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectFromCorner(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y); + } + + /** + * Scaling logic for the X axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectX(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'x' }); + } + + /** + * Scaling logic for the Y axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectY(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'y' }); + } + + /** + * Composed action handler to either scale Y or skew X + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingYOrSkewingX(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerX(eventData, transform, x, y); + } + return controls.scalingY(eventData, transform, x, y); + } + + /** + * Composed action handler to either scale X or skew Y + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingXOrSkewingY(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerY(eventData, transform, x, y); + } + return controls.scalingX(eventData, transform, x, y); + } + + /** + * Action handler to change textbox width + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function changeWidth(eventData, transform, x, y) { + var target = transform.target, localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), + multiplier = isTransformCentered(transform) ? 2 : 1, + oldWidth = target.width, + newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; + target.set('width', Math.max(newWidth, 0)); + return oldWidth !== newWidth; + } + + /** + * Action handler + * @private + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if the translation occurred + */ + function dragHandler(eventData, transform, x, y) { + var target = transform.target, + newLeft = x - transform.offsetX, + newTop = y - transform.offsetY, + moveX = !target.get('lockMovementX') && target.left !== newLeft, + moveY = !target.get('lockMovementY') && target.top !== newTop; + moveX && target.set('left', newLeft); + moveY && target.set('top', newTop); + if (moveX || moveY) { + fireEvent('moving', commonEventInfo(eventData, transform, x, y)); + } + return moveX || moveY; + } + + controls.scaleCursorStyleHandler = scaleCursorStyleHandler; + controls.skewCursorStyleHandler = skewCursorStyleHandler; + controls.scaleSkewCursorStyleHandler = scaleSkewCursorStyleHandler; + controls.rotationWithSnapping = wrapWithFireEvent('rotating', wrapWithFixedAnchor(rotationWithSnapping)); + controls.scalingEqually = wrapWithFireEvent('scaling', wrapWithFixedAnchor( scaleObjectFromCorner)); + controls.scalingX = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectX)); + controls.scalingY = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectY)); + controls.scalingYOrSkewingX = scalingYOrSkewingX; + controls.scalingXOrSkewingY = scalingXOrSkewingY; + controls.changeWidth = wrapWithFireEvent('resizing', wrapWithFixedAnchor(changeWidth)); + controls.skewHandlerX = skewHandlerX; + controls.skewHandlerY = skewHandlerY; + controls.dragHandler = dragHandler; + controls.scaleOrSkewActionName = scaleOrSkewActionName; + controls.rotationStyleHandler = rotationStyleHandler; + controls.fireEvent = fireEvent; + controls.wrapWithFixedAnchor = wrapWithFixedAnchor; + controls.wrapWithFireEvent = wrapWithFireEvent; + controls.getLocalPoint = getLocalPoint; + fabric.controlsUtils = controls; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + degreesToRadians = fabric.util.degreesToRadians, + controls = fabric.controlsUtils; + + /** + * Render a round control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderCircleControl (ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor), + myLeft = left, + myTop = top, size; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // as soon as fabric react v5, remove ie11, use proper ellipse code. + if (xSize > ySize) { + size = xSize; + ctx.scale(1.0, ySize / xSize); + myTop = top * xSize / ySize; + } + else if (ySize > xSize) { + size = ySize; + ctx.scale(xSize / ySize, 1.0); + myLeft = left * ySize / xSize; + } + else { + size = xSize; + } + // this is still wrong + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false); + ctx[methodName](); + if (stroke) { + ctx.stroke(); + } + ctx.restore(); + } + + /** + * Render a square control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderSquareControl(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && ( + styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor + ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // this is still wrong + ctx.lineWidth = 1; + ctx.translate(left, top); + ctx.rotate(degreesToRadians(fabricObject.angle)); + // this does not work, and fixed with ( && ) does not make sense. + // to have real transparent corners we need the controls on upperCanvas + // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize); + if (stroke) { + ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + } + ctx.restore(); + } + + controls.renderCircleControl = renderCircleControl; + controls.renderSquareControl = renderSquareControl; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + function Control(options) { + for (var i in options) { + this[i] = options[i]; + } + } + + fabric.Control = Control; + + fabric.Control.prototype = /** @lends fabric.Control.prototype */ { + + /** + * keep track of control visibility. + * mainly for backward compatibility. + * if you do not want to see a control, you can remove it + * from the controlset. + * @type {Boolean} + * @default true + */ + visible: true, + + /** + * Name of the action that the control will likely execute. + * This is optional. FabricJS uses to identify what the user is doing for some + * extra optimizations. If you are writing a custom control and you want to know + * somewhere else in the code what is going on, you can use this string here. + * you can also provide a custom getActionName if your control run multiple actions + * depending on some external state. + * default to scale since is the most common, used on 4 corners by default + * @type {String} + * @default 'scale' + */ + actionName: 'scale', + + /** + * Drawing angle of the control. + * NOT used for now, but name marked as needed for internal logic + * example: to reuse the same drawing function for different rotated controls + * @type {Number} + * @default 0 + */ + angle: 0, + + /** + * Relative position of the control. X + * 0,0 is the center of the Object, while -0.5 (left) or 0.5 (right) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + x: 0, + + /** + * Relative position of the control. Y + * 0,0 is the center of the Object, while -0.5 (top) or 0.5 (bottom) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + y: 0, + + /** + * Horizontal offset of the control from the defined position. In pixels + * Positive offset moves the control to the right, negative to the left. + * It used when you want to have position of control that does not scale with + * the bounding box. Example: rotation control is placed at x:0, y: 0.5 on + * the boundindbox, with an offset of 30 pixels vertically. Those 30 pixels will + * stay 30 pixels no matter how the object is big. Another example is having 2 + * controls in the corner, that stay in the same position when the object scale. + * of the bounding box. + * @type {Number} + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset of the control from the defined position. In pixels + * Positive offset moves the control to the bottom, negative to the top. + * @type {Number} + * @default 0 + */ + offsetY: 0, + + /** + * Sets the length of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeX: null, + + /** + * Sets the height of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeY: null, + + /** + * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeX: null, + + /** + * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeY: null, + + /** + * Css cursor style to display when the control is hovered. + * if the method `cursorStyleHandler` is provided, this property is ignored. + * @type {String} + * @default 'crosshair' + */ + cursorStyle: 'crosshair', + + /** + * If controls has an offsetY or offsetX, draw a line that connects + * the control to the bounding box + * @type {Boolean} + * @default false + */ + withConnection: false, + + /** + * The control actionHandler, provide one to handle action ( control being moved ) + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + actionHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control handler for mouse down, provide one to handle mouse down on control + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseDownHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control mouseUpHandler, provide one to handle an effect on mouse up. + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseUpHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * Returns control actionHandler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getActionHandler: function(/* eventData, fabricObject, control */) { + return this.actionHandler; + }, + + /** + * Returns control mouseDown handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseDownHandler: function(/* eventData, fabricObject, control */) { + return this.mouseDownHandler; + }, + + /** + * Returns control mouseUp handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseUpHandler: function(/* eventData, fabricObject, control */) { + return this.mouseUpHandler; + }, + + /** + * Returns control cursorStyle for css using cursorStyle. If you need a more elaborate + * function you can pass one in the constructor + * the cursorStyle property + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + cursorStyleHandler: function(eventData, control /* fabricObject */) { + return control.cursorStyle; + }, + + /** + * Returns the action name. The basic implementation just return the actionName property. + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + getActionName: function(eventData, control /* fabricObject */) { + return control.actionName; + }, + + /** + * Returns controls visibility + * @param {fabric.Object} object on which the control is displayed + * @param {String} controlKey key where the control is memorized on the + * @return {Boolean} + */ + getVisibility: function(fabricObject, controlKey) { + var objectVisibility = fabricObject._controlsVisibility; + if (objectVisibility && typeof objectVisibility[controlKey] !== 'undefined') { + return objectVisibility[controlKey]; + } + return this.visible; + }, + + /** + * Sets controls visibility + * @param {Boolean} visibility for the object + * @return {Void} + */ + setVisibility: function(visibility /* name, fabricObject */) { + this.visible = visibility; + }, + + + positionHandler: function(dim, finalMatrix /*, fabricObject, currentControl */) { + var point = fabric.util.transformPoint({ + x: this.x * dim.x + this.offsetX, + y: this.y * dim.y + this.offsetY }, finalMatrix); + return point; + }, + + /** + * Returns the coords for this control based on object values. + * @param {Number} objectAngle angle from the fabric object holding the control + * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if + * isTouch is true) + * @param {Number} centerX x coordinate where the control center should be + * @param {Number} centerY y coordinate where the control center should be + * @param {boolean} isTouch true if touch corner, false if normal corner + */ + calcCornerCoords: function(objectAngle, objectCornerSize, centerX, centerY, isTouch) { + var cosHalfOffset, + sinHalfOffset, + cosHalfOffsetComp, + sinHalfOffsetComp, + xSize = (isTouch) ? this.touchSizeX : this.sizeX, + ySize = (isTouch) ? this.touchSizeY : this.sizeY; + if (xSize && ySize && xSize !== ySize) { + // handle rectangular corners + var controlTriangleAngle = Math.atan2(ySize, xSize); + var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; + var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta); + // use complementary angle for two corners + cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp); + sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp); + } + else { + // handle square corners + // use default object corner size unless size is defined + var cornerSize = (xSize && ySize) ? xSize : objectCornerSize; + /* 0.7071067812 stands for sqrt(2)/2 */ + cornerHypotenuse = cornerSize * 0.7071067812; + // complementary angles are equal since they're both 45 degrees + var newTheta = fabric.util.degreesToRadians(45 - objectAngle); + cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta); + } + + return { + tl: { + x: centerX - sinHalfOffsetComp, + y: centerY - cosHalfOffsetComp, + }, + tr: { + x: centerX + cosHalfOffset, + y: centerY - sinHalfOffset, + }, + bl: { + x: centerX - cosHalfOffset, + y: centerY + sinHalfOffset, + }, + br: { + x: centerX + sinHalfOffsetComp, + y: centerY + cosHalfOffsetComp, + }, + }; + }, + + /** + * Render function for the control. + * When this function runs the context is unscaled. unrotate. Just retina scaled. + * all the functions will have to translate to the point left,top before starting Drawing + * if they want to draw a control where the position is detected. + * left and top are the result of the positionHandler function + * @param {RenderingContext2D} ctx the context where the control will be drawn + * @param {Number} left position of the canvas where we are about to render the control. + * @param {Number} top position of the canvas where we are about to render the control. + * @param {Object} styleOverride + * @param {fabric.Object} fabricObject the object where the control is about to be rendered + */ + render: function(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + switch (styleOverride.cornerStyle || fabricObject.cornerStyle) { + case 'circle': + fabric.controlsUtils.renderCircleControl.call(this, ctx, left, top, styleOverride, fabricObject); + break; + default: + fabric.controlsUtils.renderSquareControl.call(this, ctx, left, top, styleOverride, fabricObject); + } + }, + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + /* _FROM_SVG_START_ */ + function getColorStop(el, multiplier) { + var style = el.getAttribute('style'), + offset = el.getAttribute('offset') || 0, + color, colorAlpha, opacity, i; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length - 1] === '') { + keyValuePairs.pop(); + } + + for (i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + color = value; + } + else if (key === 'stop-opacity') { + opacity = value; + } + } + } + + if (!color) { + color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; + } + if (!opacity) { + opacity = el.getAttribute('stop-opacity'); + } + + color = new fabric.Color(color); + colorAlpha = color.getAlpha(); + opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); + opacity *= colorAlpha * multiplier; + + return { + offset: offset, + color: color.toRgb(), + opacity: opacity + }; + } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } + /* _FROM_SVG_END_ */ + + var clone = fabric.util.object.clone; + + /** + * Gradient class + * @class fabric.Gradient + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition + */ + fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { + + /** + * Horizontal offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetY: 0, + + /** + * A transform matrix to apply to the gradient before painting. + * Imported from svg gradients, is not applied with the current transform in the center. + * Before this transform is applied, the origin point is at the top left corner of the object + * plus the addition of offsetY and offsetX. + * @type Number[] + * @default null + */ + gradientTransform: null, + + /** + * coordinates units for coords. + * If `pixels`, the number of coords are in the same unit of width / height. + * If set as `percentage` the coords are still a number, but 1 means 100% of width + * for the X and 100% of the height for the y. It can be bigger than 1 and negative. + * allowed values pixels or percentage. + * @type String + * @default 'pixels' + */ + gradientUnits: 'pixels', + + /** + * Gradient type linear or radial + * @type String + * @default 'pixels' + */ + type: 'linear', + + /** + * Constructor + * @param {Object} options Options object with type, coords, gradientUnits and colorStops + * @param {Object} [options.type] gradient type linear or radial + * @param {Object} [options.gradientUnits] gradient units + * @param {Object} [options.offsetX] SVG import compatibility + * @param {Object} [options.offsetY] SVG import compatibility + * @param {Object[]} options.colorStops contains the colorstops. + * @param {Object} options.coords contains the coords of the gradient + * @param {Number} [options.coords.x1] X coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.y1] Y coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.x2] X coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.y2] Y coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.r1] only for radial gradient, radius of the inner circle + * @param {Number} [options.coords.r2] only for radial gradient, radius of the external circle + * @return {fabric.Gradient} thisArg + */ + initialize: function(options) { + options || (options = { }); + options.coords || (options.coords = { }); + + var coords, _this = this; + + // sets everything, then coords and colorstops get sets again + Object.keys(options).forEach(function(option) { + _this[option] = options[option]; + }); + + if (this.id) { + this.id += '_' + fabric.Object.__uid++; + } + else { + this.id = fabric.Object.__uid++; + } + + coords = { + x1: options.coords.x1 || 0, + y1: options.coords.y1 || 0, + x2: options.coords.x2 || 0, + y2: options.coords.y2 || 0 + }; + + if (this.type === 'radial') { + coords.r1 = options.coords.r1 || 0; + coords.r2 = options.coords.r2 || 0; + } + + this.coords = coords; + this.colorStops = options.colorStops.slice(); + }, + + /** + * Adds another colorStop + * @param {Object} colorStop Object with offset and color + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStops) { + for (var position in colorStops) { + var color = new fabric.Color(colorStops[position]); + this.colorStops.push({ + offset: parseFloat(position), + color: color.toRgb(), + opacity: color.getAlpha() + }); + } + return this; + }, + + /** + * Returns object representation of a gradient + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} + */ + toObject: function(propertiesToInclude) { + var object = { + type: this.type, + coords: this.coords, + colorStops: this.colorStops, + offsetX: this.offsetX, + offsetY: this.offsetY, + gradientUnits: this.gradientUnits, + gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an gradient + * @param {Object} object Object to create a gradient for + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object, options) { + var coords = clone(this.coords, true), i, len, options = options || {}, + markup, commonAttributes, colorStops = clone(this.colorStops, true), + needsSwap = coords.r1 > coords.r2, + transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), + offsetX = -this.offsetX, offsetY = -this.offsetY, + withViewport = !!options.additionalTransform, + gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; + // colorStops must be sorted ascending + colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (gradientUnits === 'objectBoundingBox') { + offsetX /= object.width; + offsetY /= object.height; + } + else { + offsetX += object.width / 2; + offsetY += object.height / 2; + } + if (object.type === 'path' && this.gradientUnits !== 'percentage') { + offsetX -= object.pathOffset.x; + offsetY -= object.pathOffset.y; + } + + + transform[4] -= offsetX; + transform[5] -= offsetY; + + commonAttributes = 'id="SVGID_' + this.id + + '" gradientUnits="' + gradientUnits + '"'; + commonAttributes += ' gradientTransform="' + (withViewport ? + options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; + + if (this.type === 'linear') { + markup = [ + '\n' + ]; + } + else if (this.type === 'radial') { + // svg radial gradient has just 1 radius. the biggest. + markup = [ + '\n' + ]; + } + + if (this.type === 'radial') { + if (needsSwap) { + // svg goes from internal to external radius. if radius are inverted, swap color stops. + colorStops = colorStops.concat(); + colorStops.reverse(); + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset = 1 - colorStops[i].offset; + } + } + var minRadius = Math.min(coords.r1, coords.r2); + if (minRadius > 0) { + // i have to shift all colorStops and add new one in 0. + var maxRadius = Math.max(coords.r1, coords.r2), + percentageShift = minRadius / maxRadius; + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); + } + } + } + + for (i = 0, len = colorStops.length; i < len; i++) { + var colorStop = colorStops[i]; + markup.push( + '\n' + ); + } + + markup.push((this.type === 'linear' ? '\n' : '\n')); + + return markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {CanvasGradient} + */ + toLive: function(ctx) { + var gradient, coords = fabric.util.object.clone(this.coords), i, len; + + if (!this.type) { + return; + } + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + coords.x1, coords.y1, coords.x2, coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); + } + + for (i = 0, len = this.colorStops.length; i < len; i++) { + var color = this.colorStops[i].color, + opacity = this.colorStops[i].opacity, + offset = this.colorStops[i].offset; + + if (typeof opacity !== 'undefined') { + color = new fabric.Color(color).setAlpha(opacity).toRgba(); + } + gradient.addColorStop(offset, color); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /* _FROM_SVG_START_ */ + /** + * Returns {@link fabric.Gradient} instance from an SVG element + * @static + * @memberOf fabric.Gradient + * @param {SVGGradientElement} el SVG gradient element + * @param {fabric.Object} instance + * @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. + * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly gradients + * that uses gradientUnits as 'userSpaceOnUse' and percentages. + * @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg + * @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg + * @param {Object.number} width width part of the svg tag if viewBox is not specified + * @param {Object.number} height height part of the svg tag if viewBox is not specified + * @return {fabric.Gradient} Gradient instance + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement + */ + fromElement: function(el, instance, opacityAttr, svgOptions) { + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + */ + + var multiplier = parseFloat(opacityAttr) / (/%$/.test(opacityAttr) ? 100 : 1); + multiplier = multiplier < 0 ? 0 : multiplier > 1 ? 1 : multiplier; + if (isNaN(multiplier)) { + multiplier = 1; + } + + var colorStopEls = el.getElementsByTagName('stop'), + type, + gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : 'percentage', + gradientTransform = el.getAttribute('gradientTransform') || '', + colorStops = [], + coords, i, offsetX = 0, offsetY = 0, + transformMatrix; + if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { + type = 'linear'; + coords = getLinearCoords(el); + } + else { + type = 'radial'; + coords = getRadialCoords(el); + } + + for (i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i], multiplier)); + } + + transformMatrix = fabric.parseTransformAttribute(gradientTransform); + + __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); + + if (gradientUnits === 'pixels') { + offsetX = -instance.left; + offsetY = -instance.top; + } + + var gradient = new fabric.Gradient({ + id: el.getAttribute('id'), + type: type, + coords: coords, + colorStops: colorStops, + gradientUnits: gradientUnits, + gradientTransform: transformMatrix, + offsetX: offsetX, + offsetY: offsetY, + }); + + return gradient; + } + /* _FROM_SVG_END_ */ + }); + + /** + * @private + */ + function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { + var propValue, finalValue; + Object.keys(options).forEach(function(prop) { + propValue = options[prop]; + if (propValue === 'Infinity') { + finalValue = 1; + } + else if (propValue === '-Infinity') { + finalValue = 0; + } + else { + finalValue = parseFloat(options[prop], 10); + if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { + finalValue *= 0.01; + if (gradientUnits === 'pixels') { + // then we need to fix those percentages here in svg parsing + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + finalValue *= svgOptions.viewBoxWidth || svgOptions.width; + } + if (prop === 'y1' || prop === 'y2') { + finalValue *= svgOptions.viewBoxHeight || svgOptions.height; + } + } + } + } + options[prop] = finalValue; + }); + } +})(); + + +(function() { + + 'use strict'; + + var toFixed = fabric.util.toFixed; + + /** + * Pattern class + * @class fabric.Pattern + * @see {@link http://fabricjs.com/patterns|Pattern demo} + * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition + */ + + + fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { + + /** + * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @type String + * @default + */ + repeat: 'repeat', + + /** + * Pattern horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX: 0, + + /** + * Pattern vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY: 0, + + /** + * crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * transform matrix to change the pattern, imported from svgs. + * @type Array + * @default + */ + patternTransform: null, + + /** + * Constructor + * @param {Object} [options] Options object + * @param {Function} [callback] function to invoke after callback init. + * @return {fabric.Pattern} thisArg + */ + initialize: function(options, callback) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img, isError) { + _this.source = img; + callback && callback(_this, isError); + }, null, this.crossOrigin); + } + }, + + /** + * Returns object representation of a pattern + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of a pattern instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + source, object; + + // element + if (typeof this.source.src === 'string') { + source = this.source.src; + } + // element + else if (typeof this.source === 'object' && this.source.toDataURL) { + source = this.source.toDataURL(); + } + + object = { + type: 'pattern', + source: source, + repeat: this.repeat, + crossOrigin: this.crossOrigin, + offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), + patternTransform: this.patternTransform ? this.patternTransform.concat() : null + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a pattern + * @param {fabric.Object} object + * @return {String} SVG representation of a pattern + */ + toSVG: function(object) { + var patternSource = typeof this.source === 'function' ? this.source() : this.source, + patternWidth = patternSource.width / object.width, + patternHeight = patternSource.height / object.height, + patternOffsetX = this.offsetX / object.width, + patternOffsetY = this.offsetY / object.height, + patternImgSrc = ''; + if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { + patternHeight = 1; + if (patternOffsetY) { + patternHeight += Math.abs(patternOffsetY); + } + } + if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { + patternWidth = 1; + if (patternOffsetX) { + patternWidth += Math.abs(patternOffsetX); + } + + } + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } + + return '\n' + + '\n' + + '\n'; + }, + /* _TO_SVG_END_ */ + + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Returns an instance of CanvasPattern + * @param {CanvasRenderingContext2D} ctx Context to create pattern + * @return {CanvasPattern} + */ + toLive: function(ctx) { + var source = this.source; + // if the image failed to load, return, and allow rest to continue loading + if (!source) { + return ''; + } + + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) { + return ''; + } + if (source.naturalWidth === 0 || source.naturalHeight === 0) { + return ''; + } + } + return ctx.createPattern(source, this.repeat); + } + }); +})(); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Shadow) { + fabric.warn('fabric.Shadow is already defined.'); + return; + } + + /** + * Shadow class + * @class fabric.Shadow + * @see {@link http://fabricjs.com/shadows|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition + */ + fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { + + /** + * Shadow color + * @type String + * @default + */ + color: 'rgb(0,0,0)', + + /** + * Shadow blur + * @type Number + */ + blur: 0, + + /** + * Shadow horizontal offset + * @type Number + * @default + */ + offsetX: 0, + + /** + * Shadow vertical offset + * @type Number + * @default + */ + offsetY: 0, + + /** + * Whether the shadow should affect stroke operations + * @type Boolean + * @default + */ + affectStroke: false, + + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `false`, the shadow will scale with the object. + * When `true`, the shadow's offsetX, offsetY, and blur will not be affected by the object's scale. + * default to false + * @type Boolean + * @default + */ + nonScaling: false, + + /** + * Constructor + * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetY properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px") + * @return {fabric.Shadow} thisArg + */ + initialize: function(options) { + + if (typeof options === 'string') { + options = this._parseShadow(options); + } + + for (var prop in options) { + this[prop] = options[prop]; + } + + this.id = fabric.Object.__uid++; + }, + + /** + * @private + * @param {String} shadow Shadow value to parse + * @return {Object} Shadow object with color, offsetX, offsetY and blur + */ + _parseShadow: function(shadow) { + var shadowStr = shadow.trim(), + offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [], + color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; + + return { + color: color.trim(), + offsetX: parseFloat(offsetsAndBlur[1], 10) || 0, + offsetY: parseFloat(offsetsAndBlur[2], 10) || 0, + blur: parseFloat(offsetsAndBlur[3], 10) || 0 + }; + }, + + /** + * Returns a string representation of an instance + * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow + * @return {String} Returns CSS3 text-shadow declaration + */ + toString: function() { + return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a shadow + * @param {fabric.Object} object + * @return {String} SVG representation of a shadow + */ + toSVG: function(object) { + var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + offset = fabric.util.rotateVector( + { x: this.offsetX, y: this.offsetY }, + fabric.util.degreesToRadians(-object.angle)), + BLUR_BOX = 20, color = new fabric.Color(this.color); + + if (object.width && object.height) { + //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion + // we add some extra space to filter box to contain the blur ( 20 ) + fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + } + if (object.flipX) { + offset.x *= -1; + } + if (object.flipY) { + offset.y *= -1; + } + + return ( + '\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\n' + + '\n'); + }, + /* _TO_SVG_END_ */ + + /** + * Returns object representation of a shadow + * @return {Object} Object representation of a shadow instance + */ + toObject: function() { + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY, + affectStroke: this.affectStroke, + nonScaling: this.nonScaling + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + + ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke', 'nonScaling'].forEach(function(prop) { + if (this[prop] !== proto[prop]) { + obj[prop] = this[prop]; + } + }, this); + + return obj; + } + }); + + /** + * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") + * @static + * @field + * @memberOf fabric.Shadow + */ + // eslint-disable-next-line max-len + fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function () { + + 'use strict'; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + toFixed = fabric.util.toFixed, + transformPoint = fabric.util.transformPoint, + invertTransform = fabric.util.invertTransform, + getNodeCanvas = fabric.util.getNodeCanvas, + createCanvasElement = fabric.util.createCanvasElement, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * Static canvas class + * @class fabric.StaticCanvas + * @mixes fabric.Collection + * @mixes fabric.Observable + * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition + * @fires before:render + * @fires after:render + * @fires canvas:cleared + * @fires object:added + * @fires object:removed + */ + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + }, + + /** + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} + * @default + */ + backgroundColor: '', + + /** + * Background image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as background, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + backgroundImage: null, + + /** + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} + * @default + */ + overlayColor: '', + + /** + * Overlay image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as overlay, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + overlayImage: null, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * if set to false, takes precedence over the object value. + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @type Boolean + * @default + */ + stateful: false, + + /** + * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove}, + * {@link fabric.StaticCanvas.moveTo}, {@link fabric.StaticCanvas.clear} and many more, should also re-render canvas. + * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once + * since the renders are quequed and executed one per frame. + * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) + * Left default to true to do not break documentation and old app, fiddles. + * @type Boolean + * @default + */ + renderOnAddRemove: true, + + /** + * Indicates whether object controls (borders/controls) are rendered above overlay image + * @type Boolean + * @default + */ + controlsAboveOverlay: false, + + /** + * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas + * @type Boolean + * @default + */ + allowTouchScrolling: false, + + /** + * Indicates whether this canvas will use image smoothing, this is on by default in browsers + * @type Boolean + * @default + */ + imageSmoothingEnabled: true, + + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: fabric.iMatrix.concat(), + + /** + * if set to false background image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + backgroundVpt: true, + + /** + * if set to false overlya image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + overlayVpt: true, + + /** + * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + * @type Boolean + * @default + */ + enableRetinaScaling: true, + + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + * @memberOf fabric.StaticCanvas.prototype + */ + vptCoords: { }, + + /** + * Based on vptCoords and object.aCoords, skip rendering of objects that + * are not included in current viewport. + * May greatly help in applications with crowded canvas and use of zoom/pan + * If One of the corner of the bounding box of the object is on the canvas + * the objects get rendered. + * @memberOf fabric.StaticCanvas.prototype + * @type Boolean + * @default + */ + skipOffscreen: true, + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the canvas has rendered, and the context is placed in the + * top left corner of the canvas. + * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true + * @type fabric.Object + */ + clipPath: undefined, + + /** + * @private + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic: function(el, options) { + var cb = this.requestRenderAllBound; + this._objects = []; + this._createLowerCanvas(el); + this._initOptions(options); + // only initialize retina scaling once + if (!this.interactive) { + this._initRetinaScaling(); + } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } + this.calcOffset(); + }, + + /** + * @private + */ + _isRetinaScaling: function() { + return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling); + }, + + /** + * @private + * @return {Number} retinaScaling if applied, otherwise 1; + */ + getRetinaScaling: function() { + return this._isRetinaScaling() ? fabric.devicePixelRatio : 1; + }, + + /** + * @private + */ + _initRetinaScaling: function() { + if (!this._isRetinaScaling()) { + return; + } + var scaleRatio = fabric.devicePixelRatio; + this.__initRetinaScaling(scaleRatio, this.lowerCanvasEl, this.contextContainer); + if (this.upperCanvasEl) { + this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop); + } + }, + + __initRetinaScaling: function(scaleRatio, canvas, context) { + canvas.setAttribute('width', this.width * scaleRatio); + canvas.setAttribute('height', this.height * scaleRatio); + context.scale(scaleRatio, scaleRatio); + }, + + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + // TODO: fix stretched examples + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to + * @param {Function} callback Callback to invoke when foreground color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img, isError) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img, isError); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + image && (image.canvas = this); + callback && callback(image, false); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + + /** + * @private + */ + _createCanvasElement: function() { + var element = createCanvasElement(); + if (!element) { + throw CANVAS_INIT_ERROR; + } + if (!element.style) { + element.style = { }; + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + return element; + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions: function (options) { + var lowerCanvasEl = this.lowerCanvasEl; + this._setOptions(options); + + this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; + this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) { + return; + } + + lowerCanvasEl.width = this.width; + lowerCanvasEl.height = this.height; + + lowerCanvasEl.style.width = this.width + 'px'; + lowerCanvasEl.style.height = this.height + 'px'; + + this.viewportTransform = this.viewportTransform.slice(); + }, + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas: function (canvasEl) { + // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node + if (canvasEl && canvasEl.getContext) { + this.lowerCanvasEl = canvasEl; + } + else { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + } + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + this._originalCanvasStyle = this.lowerCanvasEl.style; + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width (in px) + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height (in px) + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @param {Number|String} value Value to set width to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value, options) { + return this.setDimensions({ width: value }, options); + }, + + /** + * Sets height of this canvas instance + * @param {Number|String} value Value to set height to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value, options) { + return this.setDimensions({ height: value }, options); + }, + + /** + * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) + * @param {Object} dimensions Object with width/height properties + * @param {Number|String} [dimensions.width] Width of canvas element + * @param {Number|String} [dimensions.height] Height of canvas element + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function (dimensions, options) { + var cssValue; + + options = options || {}; + + for (var prop in dimensions) { + cssValue = dimensions[prop]; + + if (!options.cssOnly) { + this._setBackstoreDimension(prop, dimensions[prop]); + cssValue += 'px'; + this.hasLostContext = true; + } + + if (!options.backstoreOnly) { + this._setCssDimension(prop, cssValue); + } + } + if (this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(); + } + this._initRetinaScaling(); + this.calcOffset(); + + if (!options.cssOnly) { + this.requestRenderAll(); + } + + return this; + }, + + /** + * Helper for setting width/height + * @private + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setBackstoreDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + this[prop] = value; + + return this; + }, + + /** + * Helper for setting css width/height + * @private + * @param {String} prop property (width|height) + * @param {String} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setCssDimension: function (prop, value) { + this.lowerCanvasEl.style[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl.style[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value; + } + + return this; + }, + + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return this.viewportTransform[0]; + }, + + /** + * Sets viewport transform of this canvas instance + * @param {Array} vpt the transform in the form of context.transform + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform: function (vpt) { + var activeObject = this._activeObject, + backgroundObject = this.backgroundImage, + overlayObject = this.overlayImage, + object, i, len; + this.viewportTransform = vpt; + for (i = 0, len = this._objects.length; i < len; i++) { + object = this._objects[i]; + object.group || object.setCoords(true); + } + if (activeObject) { + activeObject.setCoords(); + } + if (backgroundObject) { + backgroundObject.setCoords(true); + } + if (overlayObject) { + overlayObject.setCoords(true); + } + this.calcViewportBoundaries(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Sets zoom level of this canvas instance, the zoom centered around point + * meaning that following zoom to point with the same point will have the visual + * effect of the zoom originating from that point. The point won't move. + * It has nothing to do with canvas center or visual center of the viewport. + * @param {fabric.Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint: function (point, value) { + // TODO: just change the scale, preserve other transformations + var before = point, vpt = this.viewportTransform.slice(0); + point = transformPoint(point, invertTransform(this.viewportTransform)); + vpt[0] = value; + vpt[3] = value; + var after = transformPoint(point, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + this.zoomToPoint(new fabric.Point(0, 0), value); + return this; + }, + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {fabric.Point} point to move to + * @return {fabric.Canvas} instance + * @chainable true + */ + absolutePan: function (point) { + var vpt = this.viewportTransform.slice(0); + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); + }, + + /** + * Pans viewpoint relatively + * @param {fabric.Point} point (position vector) to move by + * @return {fabric.Canvas} instance + * @chainable true + */ + relativePan: function (point) { + return this.absolutePan(new fabric.Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + )); + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + this.fire('object:removed', { target: obj }); + obj.fire('removed'); + delete obj.canvas; + }, + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this.remove.apply(this, this.getObjects()); + this.backgroundImage = null; + this.overlayImage = null; + this.backgroundColor = ''; + this.overlayColor = ''; + if (this._hasITextHandlers) { + this.off('mouse:up', this._mouseUpITextHandler); + this._iTextInstances = null; + this._hasITextHandlers = false; + } + this.clearContext(this.contextContainer); + this.fire('canvas:cleared'); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Renders the canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._objects); + + return this; + }, + + /** + * Function created to be instance bound at initialization + * used in requestAnimationFrame rendering + * Let the fabricJS call it. If you call it manually you could have more + * animationFrame stacking on to of each other + * for an imperative rendering, use canvas.renderAll + * @private + * @return {fabric.Canvas} instance + * @chainable + */ + renderAndReset: function() { + this.isRendering = 0; + this.renderAll(); + }, + + /** + * Append a renderAll request to next animation frame. + * unless one is already in progress, in that case nothing is done + * a boolean flag will avoid appending more. + * @return {fabric.Canvas} instance + * @chainable + */ + requestRenderAll: function () { + if (!this.isRendering) { + this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound); + } + return this; + }, + + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries: function() { + var points = { }, width = this.width, height = this.height, + iVpt = invertTransform(this.viewportTransform); + points.tl = transformPoint({ x: 0, y: 0 }, iVpt); + points.br = transformPoint({ x: width, y: height }, iVpt); + points.tr = new fabric.Point(points.br.x, points.tl.y); + points.bl = new fabric.Point(points.tl.x, points.br.y); + this.vptCoords = points; + return points; + }, + + cancelRequestedRender: function() { + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + }, + + /** + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render + * @return {fabric.Canvas} instance + * @chainable + */ + renderCanvas: function(ctx, objects) { + var v = this.viewportTransform, path = this.clipPath; + this.cancelRequestedRender(); + this.calcViewportBoundaries(); + this.clearContext(ctx); + fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled); + this.fire('before:render', { ctx: ctx, }); + this._renderBackground(ctx); + + ctx.save(); + //apply viewport transform once for all rendering process + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._renderObjects(ctx, objects); + ctx.restore(); + if (!this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + if (path) { + path.canvas = this; + // needed to setup a couple of variables + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCanvas(ctx); + } + this._renderOverlay(ctx); + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + this.fire('after:render', { ctx: ctx, }); + }, + + /** + * Paint the cached clipPath on the lowerCanvasEl + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCanvas: function(ctx) { + var v = this.viewportTransform, path = this.clipPath; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4; + ctx.globalCompositeOperation = 'destination-in'; + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} objects to render + */ + _renderObjects: function(ctx, objects) { + var i, len; + for (i = 0, len = objects.length; i < len; ++i) { + objects[i] && objects[i].render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {string} property 'background' or 'overlay' + */ + _renderBackgroundOrOverlay: function(ctx, property) { + var fill = this[property + 'Color'], object = this[property + 'Image'], + v = this.viewportTransform, needsVpt = this[property + 'Vpt']; + if (!fill && !object) { + return; + } + if (fill) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(this.width, 0); + ctx.lineTo(this.width, this.height); + ctx.lineTo(0, this.height); + ctx.closePath(); + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx, this) + : fill; + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); + var m = fill.gradientTransform || fill.patternTransform; + m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + ctx.fill(); + ctx.restore(); + } + if (object) { + ctx.save(); + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + object.render(ctx); + ctx.restore(); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'background'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'overlay'); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.height / 2, + left: this.width / 2 + }; + }, + + /** + * Centers object horizontally in the canvas + * @param {fabric.Object} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + }, + + /** + * Centers object vertically in the canvas + * @param {fabric.Object} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + }, + + /** + * Centers object vertically and horizontally in the canvas + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function(object) { + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); + }, + + /** + * Centers object vertically and horizontally in the viewport + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObject: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, vpCenter); + }, + + /** + * Centers object horizontally in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectH: function(object) { + var vpCenter = this.getVpCenter(); + this._centerObject(object, new fabric.Point(vpCenter.x, object.getCenterPoint().y)); + return this; + }, + + /** + * Centers object Vertically in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectV: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, vpCenter.y)); + }, + + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {fabric.Point} vpCenter, viewport center + * @chainable + */ + getVpCenter: function() { + var center = this.getCenter(), + iVpt = invertTransform(this.viewportTransform); + return transformPoint({ x: center.left, y: center.top }, iVpt); + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Returns dataless JSON representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} json string + */ + toDatalessJSON: function (propertiesToInclude) { + return this.toDatalessObject(propertiesToInclude); + }, + + /** + * Returns object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function (propertiesToInclude) { + return this._toObjectMethod('toObject', propertiesToInclude); + }, + + /** + * Returns dataless object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function (propertiesToInclude) { + return this._toObjectMethod('toDatalessObject', propertiesToInclude); + }, + + /** + * @private + */ + _toObjectMethod: function (methodName, propertiesToInclude) { + + var clipPath = this.clipPath, data = { + version: fabric.version, + objects: this._toObjects(methodName, propertiesToInclude), + }; + if (clipPath && !clipPath.excludeFromExport) { + data.clipPath = this._toObject(this.clipPath, methodName, propertiesToInclude); + } + extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); + + fabric.util.populateWithProperties(this, data, propertiesToInclude); + + return data; + }, + + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this._objects.filter(function(object) { + return !object.excludeFromExport; + }).map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function(methodName, propertiesToInclude) { + var data = {}, bgImage = this.backgroundImage, overlayImage = this.overlayImage, + bgColor = this.backgroundColor, overlayColor = this.overlayColor; + + if (bgColor && bgColor.toObject) { + if (!bgColor.excludeFromExport) { + data.background = bgColor.toObject(propertiesToInclude); + } + } + else if (bgColor) { + data.background = bgColor; + } + + if (overlayColor && overlayColor.toObject) { + if (!overlayColor.excludeFromExport) { + data.overlay = overlayColor.toObject(propertiesToInclude); + } + } + else if (overlayColor) { + data.overlay = overlayColor; + } + + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject(bgImage, methodName, propertiesToInclude); + } + if (overlayImage && !overlayImage.excludeFromExport) { + data.overlayImage = this._toObject(overlayImage, methodName, propertiesToInclude); + } + + return data; + }, + + /* _TO_SVG_START_ */ + /** + * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, + * a zoomed canvas will then produce zoomed SVG output. + * @type Boolean + * @default + */ + svgViewportTransformation: true, + + /** + * Returns SVG representation of canvas + * @function + * @param {Object} [options] Options object for SVG output + * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included + * @param {Object} [options.viewBox] SVG viewbox object + * @param {Number} [options.viewBox.x] x-coordinate of viewbox + * @param {Number} [options.viewBox.y] y-coordinate of viewbox + * @param {Number} [options.viewBox.width] Width of viewbox + * @param {Number} [options.viewBox.height] Height of viewbox + * @param {String} [options.encoding=UTF-8] Encoding of SVG output + * @param {String} [options.width] desired width of svg with or without units + * @param {String} [options.height] desired height of svg with or without units + * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. + * @return {String} SVG string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} + * @example Normal SVG output + * var svg = canvas.toSVG(); + * @example SVG output without preamble (without <?xml ../>) + * var svg = canvas.toSVG({suppressPreamble: true}); + * @example SVG output with viewBox attribute + * var svg = canvas.toSVG({ + * viewBox: { + * x: 100, + * y: 100, + * width: 200, + * height: 300 + * } + * }); + * @example SVG output with different encoding (default: UTF-8) + * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); + * @example Modify SVG output with reviver function + * var svg = canvas.toSVG(null, function(svg) { + * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); + * }); + */ + toSVG: function(options, reviver) { + options || (options = { }); + options.reviver = reviver; + var markup = []; + + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'background'); + this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); + this._setSVGObjects(markup, reviver); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'overlay'); + this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { + var width = options.width || this.width, + height = options.height || this.height, + vpt, viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + if (options.viewBox) { + viewBox = 'viewBox="' + + options.viewBox.x + ' ' + + options.viewBox.y + ' ' + + options.viewBox.width + ' ' + + options.viewBox.height + '" '; + } + else { + if (this.svgViewportTransformation) { + vpt = this.viewportTransform; + viewBox = 'viewBox="' + + toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + '" '; + } + } + + markup.push( + '\n', + 'Created with Fabric.js ', fabric.version, '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + this.createSVGClipPathMarkup(options), + '\n' + ); + }, + + createSVGClipPathMarkup: function(options) { + var clipPath = this.clipPath; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + return '\n' + + this.clipPath.toClipPathSVG(options.reviver) + + '\n'; + } + return ''; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup: function() { + var _this = this, + markup = ['background', 'overlay'].map(function(prop) { + var fill = _this[prop + 'Color']; + if (fill && fill.toLive) { + var shouldTransform = _this[prop + 'Vpt'], vpt = _this.viewportTransform, + object = { + width: _this.width / (shouldTransform ? vpt[0] : 1), + height: _this.height / (shouldTransform ? vpt[3] : 1) + }; + return fill.toSVG( + object, + { additionalTransform: shouldTransform ? fabric.util.matrixToSVG(vpt) : '' } + ); + } + }); + return markup.join(''); + }, + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function() { + var markup = '', fontList = { }, obj, fontFamily, + style, row, rowIndex, _char, charIndex, i, len, + fontPaths = fabric.fontPaths, objects = []; + + this._objects.forEach(function add(object) { + objects.push(object); + if (object._objects) { + object._objects.forEach(add); + } + }); + + for (i = 0, len = objects.length; i < len; i++) { + obj = objects[i]; + fontFamily = obj.fontFamily; + if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { + continue; + } + fontList[fontFamily] = true; + if (!obj.styles) { + continue; + } + style = obj.styles; + for (rowIndex in style) { + row = style[rowIndex]; + for (charIndex in row) { + _char = row[charIndex]; + fontFamily = _char.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; + } + } + } + } + + for (var j in fontList) { + markup += [ + '\t\t@font-face {\n', + '\t\t\tfont-family: \'', j, '\';\n', + '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', + '\t\t}\n' + ].join(''); + } + + if (markup) { + markup = [ + '\t\n' + ].join(''); + } + + return markup; + }, + + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { + var instance, i, len, objects = this._objects; + for (i = 0, len = objects.length; i < len; i++) { + instance = objects[i]; + if (instance.excludeFromExport) { + continue; + } + this._setSVGObject(markup, instance, reviver); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + markup.push(instance.toSVG(reviver)); + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property, reviver) { + if (this[property] && !this[property].excludeFromExport && this[property].toSVG) { + markup.push(this[property].toSVG(reviver)); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + var filler = this[property + 'Color'], vpt = this.viewportTransform, finalWidth = this.width, + finalHeight = this.height; + if (!filler) { + return; + } + if (filler.toLive) { + var repeat = filler.repeat, iVpt = fabric.util.invertTransform(vpt), shouldInvert = this[property + 'Vpt'], + additionalTransform = shouldInvert ? fabric.util.matrixToSVG(iVpt) : ''; + markup.push( + '\n' + ); + } + else { + markup.push( + '\n' + ); + } + }, + /* _TO_SVG_END_ */ + + /** + * Moves an object or the objects of a multiple selection + * to the bottom of the stack of drawn objects + * @param {fabric.Object} object Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.unshift(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.unshift(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or the objects of a multiple selection + * to the top of the stack of drawn objects + * @param {fabric.Object} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.push(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.push(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or a selection down in stack of drawn objects + * An optional parameter, intersecting allows to move the object in behind + * the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx > 0 + objsMoved) { + newIdx = idx - 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== 0) { + // if object is not on the bottom of stack + newIdx = this._findNewLowerIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx, i; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (i = idx - 1; i >= 0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + + /** + * Moves an object or a selection up in stack of drawn objects + * An optional parameter, intersecting allows to move the object in front + * of the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx < this._objects.length - 1 - objsMoved) { + newIdx = idx + 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== this._objects.length - 1) { + // if object is not on top of stack (last item in an array) + newIdx = this._findNewUpperIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx, i, len; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (i = idx + 1, len = this._objects.length; i < len; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx + 1; + } + + return newIdx; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {fabric.Object} object Object to send + * @param {Number} index Position to move to + * @return {fabric.Canvas} thisArg + * @chainable + */ + moveTo: function (object, index) { + removeFromArray(this._objects, object); + this._objects.splice(index, 0, object); + return this.renderOnAddRemove && this.requestRenderAll(); + }, + + /** + * Clears a canvas element and dispose objects + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + // cancel eventually ongoing renders + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + this.forEachObject(function(object) { + object.dispose && object.dispose(); + }); + this._objects = []; + if (this.backgroundImage && this.backgroundImage.dispose) { + this.backgroundImage.dispose(); + } + this.backgroundImage = null; + if (this.overlayImage && this.overlayImage.dispose) { + this.overlayImage.dispose(); + } + this.overlayImage = null; + this._iTextInstances = null; + this.contextContainer = null; + // restore canvas style + this.lowerCanvasEl.classList.remove('lower-canvas'); + this.lowerCanvasEl.style = this._originalCanvasStyle; + delete this._originalCanvasStyle; + // restore canvas size to original size in case retina scaling was applied + this.lowerCanvasEl.setAttribute('width', this.width); + this.lowerCanvasEl.setAttribute('height', this.height); + fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); + this.lowerCanvasEl = undefined; + return this; + }, + + /** + * Returns a string representation of an instance + * @return {String} string representation of an instance + */ + toString: function () { + return '#'; + } + }); + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + extend(fabric.StaticCanvas.prototype, fabric.Collection); + extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); + + extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { + + /** + * @static + * @type String + * @default + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @param {String} methodName Method to check support for; + * Could be one of "setLineDash" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = createCanvasElement(); + + if (!el || !el.getContext) { + return null; + } + + var ctx = el.getContext('2d'); + if (!ctx) { + return null; + } + + switch (methodName) { + + case 'setLineDash': + return typeof ctx.setLineDash !== 'undefined'; + + default: + return null; + } + } + }); + + /** + * Returns Object representation of canvas + * this alias is provided because if you call JSON.stringify on an instance, + * the toJSON object will be invoked if it exists. + * Having a toJSON method means you can do JSON.stringify(myCanvas) + * @function + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON compatible object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} + * @example JSON without additional properties + * var json = canvas.toJSON(); + * @example JSON with additional properties included + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); + * @example JSON without default values + * canvas.includeDefaultValues = false; + * var json = canvas.toJSON(); + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + + if (fabric.isLikelyNode) { + fabric.StaticCanvas.prototype.createPNGStream = function() { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createPNGStream(); + }; + fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createJPEGStream(opts); + }; + } +})(); + + +/** + * BaseBrush class + * @class fabric.BaseBrush + * @see {@link http://fabricjs.com/freedrawing|Freedrawing demo} + */ +fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ { + + /** + * Color of a brush + * @type String + * @default + */ + color: 'rgb(0, 0, 0)', + + /** + * Width of a brush, has to be a Number, no string literals + * @type Number + * @default + */ + width: 1, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), + * "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Line endings style of a brush (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'round', + + /** + * Corner style of a brush (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'round', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of a brush's + * @type Number + * @default + */ + strokeMiterLimit: 10, + + /** + * Stroke Dash Array. + * @type Array + * @default + */ + strokeDashArray: null, + + /** + * When `true`, the free drawing is limited to the whiteboard size. Default to false. + * @type Boolean + * @default false + */ + + limitedToCanvasSize: false, + + + /** + * Sets brush styles + * @private + */ + _setBrushStyles: function() { + var ctx = this.canvas.contextTop; + ctx.strokeStyle = this.color; + ctx.lineWidth = this.width; + ctx.lineCap = this.strokeLineCap; + ctx.miterLimit = this.strokeMiterLimit; + ctx.lineJoin = this.strokeLineJoin; + ctx.setLineDash(this.strokeDashArray || []); + }, + + /** + * Sets the transformation on given context + * @param {RenderingContext2d} ctx context to render on + * @private + */ + _saveAndTransform: function(ctx) { + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + }, + + /** + * Sets brush shadow styles + * @private + */ + _setShadow: function() { + if (!this.shadow) { + return; + } + + var canvas = this.canvas, + shadow = this.shadow, + ctx = canvas.contextTop, + zoom = canvas.getZoom(); + if (canvas && canvas._isRetinaScaling()) { + zoom *= fabric.devicePixelRatio; + } + + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * zoom; + ctx.shadowOffsetX = shadow.offsetX * zoom; + ctx.shadowOffsetY = shadow.offsetY * zoom; + }, + + needsFullRender: function() { + var color = new fabric.Color(this.color); + return color.getAlpha() < 1 || !!this.shadow; + }, + + /** + * Removes brush shadow styles + * @private + */ + _resetShadow: function() { + var ctx = this.canvas.contextTop; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * Check is pointer is outside canvas boundaries + * @param {Object} pointer + * @private + */ + _isOutSideCanvas: function(pointer) { + return pointer.x < 0 || pointer.x > this.canvas.getWidth() || pointer.y < 0 || pointer.y > this.canvas.getHeight(); + } +}); + + +(function() { + /** + * PencilBrush class + * @class fabric.PencilBrush + * @extends fabric.BaseBrush + */ + fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { + + /** + * Discard points that are less than `decimate` pixel distant from each other + * @type Number + * @default 0.4 + */ + decimate: 0.4, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.PencilBrush} Instance of a pencil brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this._points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + _drawSegment: function (ctx, p1, p2) { + var midPoint = p1.midPointFrom(p2); + ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); + return midPoint; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this._prepareForDrawing(pointer); + // capture coordinates immediately + // this allows to draw dots (when movement never occurs) + this._captureDrawingPath(pointer); + this._render(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + if (this._captureDrawingPath(pointer) && this._points.length > 1) { + if (this.needsFullRender()) { + // redraw curve + // clear top canvas + this.canvas.clearContext(this.canvas.contextTop); + this._render(); + } + else { + var points = this._points, length = points.length, ctx = this.canvas.contextTop; + // draw the curve update + this._saveAndTransform(ctx); + if (this.oldEnd) { + ctx.beginPath(); + ctx.moveTo(this.oldEnd.x, this.oldEnd.y); + } + this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); + ctx.stroke(); + ctx.restore(); + } + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function(options) { + if (!this.canvas._isMainEvent(options.e)) { + return true; + } + this.oldEnd = undefined; + this._finalizeAndAddPath(); + return false; + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _prepareForDrawing: function(pointer) { + + var p = new fabric.Point(pointer.x, pointer.y); + + this._reset(); + this._addPoint(p); + this.canvas.contextTop.moveTo(p.x, p.y); + }, + + /** + * @private + * @param {fabric.Point} point Point to be added to points array + */ + _addPoint: function(point) { + if (this._points.length > 1 && point.eq(this._points[this._points.length - 1])) { + return false; + } + this._points.push(point); + return true; + }, + + /** + * Clear points array and set contextTop canvas style. + * @private + */ + _reset: function() { + this._points = []; + this._setBrushStyles(); + this._setShadow(); + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _captureDrawingPath: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + return this._addPoint(pointerPoint); + }, + + /** + * Draw a smooth path on the topCanvas using quadraticCurveTo + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + p1 = this._points[0], + p2 = this._points[1]; + + this._saveAndTransform(ctx); + ctx.beginPath(); + //if we only have 2 points in the path and they are the same + //it means that the user only clicked the canvas without moving the mouse + //then we should be drawing a dot. A path isn't drawn between two identical dots + //that's why we set them apart a bit + if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { + var width = this.width / 1000; + p1 = new fabric.Point(p1.x, p1.y); + p2 = new fabric.Point(p2.x, p2.y); + p1.x -= width; + p2.x += width; + } + ctx.moveTo(p1.x, p1.y); + + for (i = 1, len = this._points.length; i < len; i++) { + // we pick the point between pi + 1 & pi + 2 as the + // end point and p1 as our control point. + this._drawSegment(ctx, p1, p2); + p1 = this._points[i]; + p2 = this._points[i + 1]; + } + // Draw last line as a straight line while + // we wait for the next point to be able to calculate + // the bezier control point + ctx.lineTo(p1.x, p1.y); + ctx.stroke(); + ctx.restore(); + }, + + /** + * Converts points to SVG path + * @param {Array} points Array of points + * @return {(string|number)[][]} SVG path commands + */ + convertPointsToSVGPath: function (points) { + var correction = this.width / 1000; + return fabric.util.getSmoothPathFromPoints(points, correction); + }, + + /** + * @private + * @param {(string|number)[][]} pathData SVG path commands + * @returns {boolean} + */ + _isEmptySVGPath: function (pathData) { + var pathString = fabric.util.joinPath(pathData); + return pathString === 'M 0 0 Q 0 0 0 0 L 0 0'; + }, + + /** + * Creates fabric.Path object to add on canvas + * @param {(string|number)[][]} pathData Path data + * @return {fabric.Path} Path to add on canvas + */ + createPath: function(pathData) { + var path = new fabric.Path(pathData, { + fill: null, + stroke: this.color, + strokeWidth: this.width, + strokeLineCap: this.strokeLineCap, + strokeMiterLimit: this.strokeMiterLimit, + strokeLineJoin: this.strokeLineJoin, + strokeDashArray: this.strokeDashArray, + }); + if (this.shadow) { + this.shadow.affectStroke = true; + path.shadow = new fabric.Shadow(this.shadow); + } + + return path; + }, + + /** + * Decimate points array with the decimate value + */ + decimatePoints: function(points, distance) { + if (points.length <= 2) { + return points; + } + var zoom = this.canvas.getZoom(), adjustedDistance = Math.pow(distance / zoom, 2), + i, l = points.length - 1, lastPoint = points[0], newPoints = [lastPoint], + cDistance; + for (i = 1; i < l - 1; i++) { + cDistance = Math.pow(lastPoint.x - points[i].x, 2) + Math.pow(lastPoint.y - points[i].y, 2); + if (cDistance >= adjustedDistance) { + lastPoint = points[i]; + newPoints.push(lastPoint); + } + } + /** + * Add the last point from the original line to the end of the array. + * This ensures decimate doesn't delete the last point on the line, and ensures the line is > 1 point. + */ + newPoints.push(points[l]); + return newPoints; + }, + + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. + */ + _finalizeAndAddPath: function() { + var ctx = this.canvas.contextTop; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points); + if (this._isEmptySVGPath(pathData)) { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.requestRenderAll(); + return; + } + + var path = this.createPath(pathData); + this.canvas.clearContext(this.canvas.contextTop); + this.canvas.fire('before:path:created', { path: path }); + this.canvas.add(path); + this.canvas.requestRenderAll(); + path.setCoords(); + this._resetShadow(); + + + // fire event 'path' created + this.canvas.fire('path:created', { path: path }); + } + }); +})(); + + +/** + * CircleBrush class + * @class fabric.CircleBrush + */ +fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { + + /** + * Width of a brush + * @type Number + * @default + */ + width: 10, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.CircleBrush} Instance of a circle brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + drawDot: function(pointer) { + var point = this.addPoint(pointer), + ctx = this.canvas.contextTop; + this._saveAndTransform(ctx); + this.dot(ctx, point); + ctx.restore(); + }, + + dot: function(ctx, point) { + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + }, + + /** + * Invoked on mouse down + */ + onMouseDown: function(pointer) { + this.points.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + this.drawDot(pointer); + }, + + /** + * Render the full state of the brush + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + points = this.points; + this._saveAndTransform(ctx); + for (i = 0, len = points.length; i < len; i++) { + this.dot(ctx, points[i]); + } + ctx.restore(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + if (this.needsFullRender()) { + this.canvas.clearContext(this.canvas.contextTop); + this.addPoint(pointer); + this._render(); + } + else { + this.drawDot(pointer); + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove, i, len; + this.canvas.renderOnAddRemove = false; + + var circles = []; + + for (i = 0, len = this.points.length; i < len; i++) { + var point = this.points[i], + circle = new fabric.Circle({ + radius: point.radius, + left: point.x, + top: point.y, + originX: 'center', + originY: 'center', + fill: point.fill + }); + + this.shadow && (circle.shadow = new fabric.Shadow(this.shadow)); + + circles.push(circle); + } + var group = new fabric.Group(circles); + group.canvas = this.canvas; + + this.canvas.fire('before:path:created', { path: group }); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @param {Object} pointer + * @return {fabric.Point} Just added pointer point + */ + addPoint: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y), + + circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2, + + circleColor = new fabric.Color(this.color) + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); + + pointerPoint.radius = circleRadius; + pointerPoint.fill = circleColor; + + this.points.push(pointerPoint); + + return pointerPoint; + } +}); + + +/** + * SprayBrush class + * @class fabric.SprayBrush + */ +fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { + + /** + * Width of a spray + * @type Number + * @default + */ + width: 10, + + /** + * Density of a spray (number of dots per chunk) + * @type Number + * @default + */ + density: 20, + + /** + * Width of spray dots + * @type Number + * @default + */ + dotWidth: 1, + + /** + * Width variance of spray dots + * @type Number + * @default + */ + dotWidthVariance: 1, + + /** + * Whether opacity of a dot should be random + * @type Boolean + * @default + */ + randomOpacity: false, + + /** + * Whether overlapping dots (rectangles) should be removed (for performance reasons) + * @type Boolean + * @default + */ + optimizeOverlapping: true, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.SprayBrush} Instance of a spray brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.sprayChunks = []; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer) { + this.sprayChunks.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + var rects = []; + + for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + var sprayChunk = this.sprayChunks[i]; + + for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) { + + var rect = new fabric.Rect({ + width: sprayChunk[j].width, + height: sprayChunk[j].width, + left: sprayChunk[j].x + 1, + top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', + fill: this.color + }); + rects.push(rect); + } + } + + if (this.optimizeOverlapping) { + rects = this._getOptimizedRects(rects); + } + + var group = new fabric.Group(rects); + this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); + this.canvas.fire('before:path:created', { path: group }); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @private + * @param {Array} rects + */ + _getOptimizedRects: function(rects) { + + // avoid creating duplicate rects at the same coordinates + var uniqueRects = { }, key, i, len; + + for (i = 0, len = rects.length; i < len; i++) { + key = rects[i].left + '' + rects[i].top; + if (!uniqueRects[key]) { + uniqueRects[key] = rects[i]; + } + } + var uniqueRectsArray = []; + for (key in uniqueRects) { + uniqueRectsArray.push(uniqueRects[key]); + } + + return uniqueRectsArray; + }, + + /** + * Render new chunk of spray brush + */ + render: function(sprayChunk) { + var ctx = this.canvas.contextTop, i, len; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, len = sprayChunk.length; i < len; i++) { + var point = sprayChunk[i]; + if (typeof point.opacity !== 'undefined') { + ctx.globalAlpha = point.opacity; + } + ctx.fillRect(point.x, point.y, point.width, point.width); + } + ctx.restore(); + }, + + /** + * Render all spray chunks + */ + _render: function() { + var ctx = this.canvas.contextTop, i, ilen; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + this.render(this.sprayChunks[i]); + } + ctx.restore(); + }, + + /** + * @param {Object} pointer + */ + addSprayChunk: function(pointer) { + this.sprayChunkPoints = []; + + var x, y, width, radius = this.width / 2, i; + + for (i = 0; i < this.density; i++) { + + x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); + y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); + + if (this.dotWidthVariance) { + width = fabric.util.getRandomInt( + // bottom clamp width to 1 + Math.max(1, this.dotWidth - this.dotWidthVariance), + this.dotWidth + this.dotWidthVariance); + } + else { + width = this.dotWidth; + } + + var point = new fabric.Point(x, y); + point.width = width; + + if (this.randomOpacity) { + point.opacity = fabric.util.getRandomInt(0, 100) / 100; + } + + this.sprayChunkPoints.push(point); + } + + this.sprayChunks.push(this.sprayChunkPoints); + } +}); + + +/** + * PatternBrush class + * @class fabric.PatternBrush + * @extends fabric.BaseBrush + */ +fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { + + getPatternSrc: function() { + + var dotWidth = 20, + dotDistance = 5, + patternCanvas = fabric.util.createCanvasElement(), + patternCtx = patternCanvas.getContext('2d'); + + patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; + + patternCtx.fillStyle = this.color; + patternCtx.beginPath(); + patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); + patternCtx.closePath(); + patternCtx.fill(); + + return patternCanvas; + }, + + getPatternSrcFunction: function() { + return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); + }, + + /** + * Creates "pattern" instance property + */ + getPattern: function() { + return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); + }, + + /** + * Sets brush styles + */ + _setBrushStyles: function() { + this.callSuper('_setBrushStyles'); + this.canvas.contextTop.strokeStyle = this.getPattern(); + }, + + /** + * Creates path + */ + createPath: function(pathData) { + var path = this.callSuper('createPath', pathData), + topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); + + path.stroke = new fabric.Pattern({ + source: this.source || this.getPatternSrcFunction(), + offsetX: -topLeft.x, + offsetY: -topLeft.y + }); + return path; + } +}); + + +(function() { + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + isTouchEvent = fabric.util.isTouchEvent; + + /** + * Canvas class + * @class fabric.Canvas + * @extends fabric.StaticCanvas + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition + * + * @fires object:modified at the end of a transform or any change when statefull is true + * @fires object:rotating while an object is being rotated from the control + * @fires object:scaling while an object is being scaled by controls + * @fires object:moving while an object is being dragged + * @fires object:skewing while an object is being skewed from the controls + * + * @fires before:transform before a transform is is started + * @fires before:selection:cleared + * @fires selection:cleared + * @fires selection:updated + * @fires selection:created + * + * @fires path:created after a drawing operation ends and the path is added + * @fires mouse:down + * @fires mouse:move + * @fires mouse:up + * @fires mouse:down:before on mouse down, before the inner fabric logic runs + * @fires mouse:move:before on mouse move, before the inner fabric logic runs + * @fires mouse:up:before on mouse up, before the inner fabric logic runs + * @fires mouse:over + * @fires mouse:out + * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + * @fires after:render at the end of the render process, receives the context in the callback + * @fires before:render at start the render process, receives the context in the callback + * + * the following events are deprecated: + * @fires object:rotated at the end of a rotation transform + * @fires object:scaled at the end of a scale transform + * @fires object:moved at the end of translation transform + * @fires object:skewed at the end of a skew transform + */ + fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + }, + + /** + * When true, objects can be transformed by one side (unproportionally) + * when dragged on the corners that normally would not do that. + * @type Boolean + * @default + * @since fabric 4.0 // changed name and default value + */ + uniformScaling: true, + + /** + * Indicates which key switches uniform scaling. + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * totally wrong named. this sounds like `uniform scaling` + * if Canvas.uniformScaling is true, pressing this will set it to false + * and viceversa. + * @since 1.6.2 + * @type String + * @default + */ + uniScaleKey: 'shiftKey', + + /** + * When true, objects use center point as the origin of scale transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, objects use center point as the origin of rotate transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: false, + + /** + * Indicates which key enable centered Transform + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + centeredKey: 'altKey', + + /** + * Indicates which key enable alternate action on corner + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + altActionKey: 'shiftKey', + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @type Boolean + * @default + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @type Boolean + * @default + */ + selection: true, + + /** + * Indicates which key or keys enable multiple click selection + * Pass value as a string or array of strings + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or empty or containing any other string that is not a modifier key + * feature is disabled. + * @since 1.6.2 + * @type String|Array + * @default + */ + selectionKey: 'shiftKey', + + /** + * Indicates which key enable alternative selection + * in case of target overlapping with active object + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * For a series of reason that come from the general expectations on how + * things should work, this feature works only for preserveObjectStacking true. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * @since 1.6.5 + * @type null|String + * @default + */ + altSelectionKey: null, + + /** + * Color of selection + * @type String + * @default + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Default dash array pattern + * If not empty the selection border is dashed + * @type Array + */ + selectionDashArray: [], + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @type String + * @default + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @type Number + * @default + */ + selectionLineWidth: 1, + + /** + * Select only shapes that are fully contained in the dragged selection rectangle. + * @type Boolean + * @default + */ + selectionFullyContained: false, + + /** + * Default cursor value used when hovering over an object on canvas + * @type String + * @default + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @type String + * @default + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @type String + * @default + */ + defaultCursor: 'default', + + /** + * Cursor value used during free drawing + * @type String + * @default + */ + freeDrawingCursor: 'crosshair', + + /** + * Cursor value used for rotation point + * @type String + * @default + */ + rotationCursor: 'crosshair', + + /** + * Cursor value used for disabled elements ( corners with disabled action ) + * @type String + * @since 2.0.0 + * @default + */ + notAllowedCursor: 'not-allowed', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @type String + * @default + */ + containerClass: 'canvas-container', + + /** + * When true, object detection happens on per-pixel basis rather than on per-bounding-box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * Number of pixels around target pixel to tolerate (consider active) during object detection + * @type Number + * @default + */ + targetFindTolerance: 0, + + /** + * When true, target detection is skipped. Target detection will return always undefined. + * click selection won't work anymore, events will fire with no targets. + * if something is selected before setting it to true, it will be deselected at the first click. + * area selection will still work. check the `selection` property too. + * if you deactivate both, you should look into staticCanvas. + * @type Boolean + * @default + */ + skipTargetFind: false, + + /** + * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. + * After mousedown, mousemove creates a shape, + * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. + * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} + * @type Boolean + * @default + */ + isDrawingMode: false, + + /** + * Indicates whether objects should remain in current stack position when selected. + * When false objects are brought to top and rendered as part of the selection group + * @type Boolean + * @default + */ + preserveObjectStacking: false, + + /** + * Indicates the angle that an object will lock to while rotating. + * @type Number + * @since 1.6.7 + * @default + */ + snapAngle: 0, + + /** + * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. + * When `null`, the snapThreshold will default to the snapAngle. + * @type null|Number + * @since 1.6.7 + * @default + */ + snapThreshold: null, + + /** + * Indicates if the right click on canvas can output the context menu or not + * @type Boolean + * @since 1.6.5 + * @default + */ + stopContextMenu: false, + + /** + * Indicates if the canvas can fire right click events + * @type Boolean + * @since 1.6.5 + * @default + */ + fireRightClick: false, + + /** + * Indicates if the canvas can fire middle click events + * @type Boolean + * @since 1.7.8 + * @default + */ + fireMiddleClick: false, + + /** + * Keep track of the subTargets for Mouse Events + * @type fabric.Object[] + */ + targets: [], + + /** + * Keep track of the hovered target + * @type fabric.Object + * @private + */ + _hoveredTarget: null, + + /** + * hold the list of nested targets hovered + * @type fabric.Object[] + * @private + */ + _hoveredTargets: [], + + /** + * @private + */ + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEventListeners(); + + this._initRetinaScaling(); + + this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + + this.calcOffset(); + }, + + /** + * Divides objects in two groups, one to render immediately + * and one to render as activeGroup. + * @return {Array} objects to render immediately and pushes the other in the activeGroup. + */ + _chooseObjectsToRender: function() { + var activeObjects = this.getActiveObjects(), + object, objsToRender, activeGroupObjects; + + if (activeObjects.length > 0 && !this.preserveObjectStacking) { + objsToRender = []; + activeGroupObjects = []; + for (var i = 0, length = this._objects.length; i < length; i++) { + object = this._objects[i]; + if (activeObjects.indexOf(object) === -1 ) { + objsToRender.push(object); + } + else { + activeGroupObjects.push(object); + } + } + if (activeObjects.length > 1) { + this._activeObject._objects = activeGroupObjects; + } + objsToRender.push.apply(objsToRender, activeGroupObjects); + } + else { + objsToRender = this._objects; + } + return objsToRender; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { + this.clearContext(this.contextTop); + this.contextTopDirty = false; + } + if (this.hasLostContext) { + this.renderTopLayer(this.contextTop); + } + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + return this; + }, + + renderTopLayer: function(ctx) { + ctx.save(); + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._render(); + this.contextTopDirty = true; + } + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(ctx); + this.contextTopDirty = true; + } + ctx.restore(); + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + var ctx = this.contextTop; + this.clearContext(ctx); + this.renderTopLayer(ctx); + this.fire('after:render'); + return this; + }, + + /** + * @private + */ + _normalizePointer: function (object, pointer) { + var m = object.calcTransformMatrix(), + invertedM = fabric.util.invertTransform(m), + vptPointer = this.restorePointerVpt(pointer); + return fabric.util.transformPoint(vptPointer, invertedM); + }, + + /** + * Returns true if object is transparent at a certain location + * @param {fabric.Object} target Object to check + * @param {Number} x Left coordinate + * @param {Number} y Top coordinate + * @return {Boolean} + */ + isTargetTransparent: function (target, x, y) { + // in case the target is the activeObject, we cannot execute this optimization + // because we need to draw controls too. + if (target.shouldCache() && target._cacheCanvas && target !== this._activeObject) { + var normalizedPointer = this._normalizePointer(target, {x: x, y: y}), + targetRelativeX = Math.max(target.cacheTranslationX + (normalizedPointer.x * target.zoomX), 0), + targetRelativeY = Math.max(target.cacheTranslationY + (normalizedPointer.y * target.zoomY), 0); + + var isTransparent = fabric.util.isTransparent( + target._cacheContext, Math.round(targetRelativeX), Math.round(targetRelativeY), this.targetFindTolerance); + + return isTransparent; + } + + var ctx = this.contextCache, + originalColor = target.selectionBackgroundColor, v = this.viewportTransform; + + target.selectionBackgroundColor = ''; + + this.clearContext(ctx); + + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + target.render(ctx); + ctx.restore(); + + target.selectionBackgroundColor = originalColor; + + var isTransparent = fabric.util.isTransparent( + ctx, x, y, this.targetFindTolerance); + + return isTransparent; + }, + + /** + * takes an event and determines if selection key has been pressed + * @private + * @param {Event} e Event object + */ + _isSelectionKeyPressed: function(e) { + var selectionKeyPressed = false; + + if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { + selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); + } + else { + selectionKeyPressed = e[this.selectionKey]; + } + + return selectionKeyPressed; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldClearSelection: function (e, target) { + var activeObjects = this.getActiveObjects(), + activeObject = this._activeObject; + + return ( + !target + || + (target && + activeObject && + activeObjects.length > 1 && + activeObjects.indexOf(target) === -1 && + activeObject !== target && + !this._isSelectionKeyPressed(e)) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * centeredScaling from object can't override centeredScaling from canvas. + * this should be fixed, since object setting should take precedence over canvas. + * also this should be something that will be migrated in the control properties. + * as ability to define the origin of the transformation that the control provide. + * @private + * @param {fabric.Object} target + * @param {String} action + * @param {Boolean} altKey + */ + _shouldCenterTransform: function (target, action, altKey) { + if (!target) { + return; + } + + var centerTransform; + + if (action === 'scale' || action === 'scaleX' || action === 'scaleY' || action === 'resizing') { + centerTransform = this.centeredScaling || target.centeredScaling; + } + else if (action === 'rotate') { + centerTransform = this.centeredRotation || target.centeredRotation; + } + + return centerTransform ? !altKey : altKey; + }, + + /** + * should disappear before release 4.0 + * @private + */ + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; + + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } + + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + return origin; + }, + + /** + * @private + * @param {Boolean} alreadySelected true if target is already selected + * @param {String} corner a string representing the corner ml, mr, tl ... + * @param {Event} e Event object + * @param {fabric.Object} [target] inserted back to help overriding. Unused + */ + _getActionFromCorner: function(alreadySelected, corner, e, target) { + if (!corner || !alreadySelected) { + return 'drag'; + } + var control = target.controls[corner]; + return control.getActionName(e, control, target); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target, alreadySelected) { + if (!target) { + return; + } + + var pointer = this.getPointer(e), corner = target.__corner, + control = target.controls[corner], + actionHandler = (alreadySelected && corner) ? + control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, + action = this._getActionFromCorner(alreadySelected, corner, e, target), + origin = this._getOriginFromCorner(target, corner), + altKey = e[this.centeredKey], + transform = { + target: target, + action: action, + actionHandler: actionHandler, + corner: corner, + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + // used by transation + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + lastX: pointer.x, + lastY: pointer.y, + // unsure they are useful anymore. + // left: target.left, + // top: target.top, + theta: degreesToRadians(target.angle), + // end of unsure + width: target.width * target.scaleX, + shiftKey: e.shiftKey, + altKey: altKey, + original: fabric.util.saveObjectTransform(target), + }; + + if (this._shouldCenterTransform(target, action, altKey)) { + transform.originX = 'center'; + transform.originY = 'center'; + } + transform.original.originX = origin.x; + transform.original.originY = origin.y; + this._currentTransform = transform; + this._beforeTransform(e); + }, + + /** + * Set the cursor type of the canvas element + * @param {String} value Cursor type of the canvas element. + * @see http://www.w3.org/TR/css3-ui/#cursor + */ + setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx to draw the selection on + */ + _drawSelection: function (ctx) { + var selector = this._groupSelector, + viewportStart = new fabric.Point(selector.ex, selector.ey), + start = fabric.util.transformPoint(viewportStart, this.viewportTransform), + viewportExtent = new fabric.Point(selector.ex + selector.left, selector.ey + selector.top), + extent = fabric.util.transformPoint(viewportExtent, this.viewportTransform), + minX = Math.min(start.x, extent.x), + minY = Math.min(start.y, extent.y), + maxX = Math.max(start.x, extent.x), + maxY = Math.max(start.y, extent.y), + strokeOffset = this.selectionLineWidth / 2; + + if (this.selectionColor) { + ctx.fillStyle = this.selectionColor; + ctx.fillRect(minX, minY, maxX - minX, maxY - minY); + } + + if (!this.selectionLineWidth || !this.selectionBorderColor) { + return; + } + ctx.lineWidth = this.selectionLineWidth; + ctx.strokeStyle = this.selectionBorderColor; + + minX += strokeOffset; + minY += strokeOffset; + maxX -= strokeOffset; + maxY -= strokeOffset; + // selection border + fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); + ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); + }, + + /** + * Method that determines what object we are clicking on + * the skipGroup parameter is for internal use, is needed for shift+click action + * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target + * or the outside part of the corner. + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through + * @return {fabric.Object} the target found + */ + findTarget: function (e, skipGroup) { + if (this.skipTargetFind) { + return; + } + + var ignoreZoom = true, + pointer = this.getPointer(e, ignoreZoom), + activeObject = this._activeObject, + aObjects = this.getActiveObjects(), + activeTarget, activeTargetSubs, + isTouch = isTouchEvent(e), + shouldLookForActive = (aObjects.length > 1 && !skipGroup) || aObjects.length === 1; + + // first check current group (if one exists) + // active group does not check sub targets like normal groups. + // if active group just exits. + this.targets = []; + + // if we hit the corner of an activeObject, let's return that. + if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { + return activeObject; + } + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + return activeObject; + } + if (aObjects.length === 1 && + activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (!this.preserveObjectStacking) { + return activeObject; + } + else { + activeTarget = activeObject; + activeTargetSubs = this.targets; + this.targets = []; + } + } + var target = this._searchPossibleTargets(this._objects, pointer); + if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { + target = activeTarget; + this.targets = activeTargetSubs; + } + return target; + }, + + /** + * Checks point is inside the object. + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @param {fabric.Object} obj Object to test against + * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. + * @return {Boolean} true if point is contained within an area of given object + * @private + */ + _checkTarget: function(pointer, obj, globalPointer) { + if (obj && + obj.visible && + obj.evented && + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + obj.containsPoint(pointer) + ) { + if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { + var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); + if (!isTransparent) { + return true; + } + } + else { + return true; + } + } + }, + + /** + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * @param {Array} [objects] objects array to look into + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @return {fabric.Object} object that contains pointer + * @private + */ + _searchPossibleTargets: function(objects, pointer) { + // Cache all targets where their bounding box contains point. + var target, i = objects.length, subTarget; + // Do not check for currently grouped objects, since we check the parent group itself. + // until we call this function specifically to search inside the activeGroup + while (i--) { + var objToCheck = objects[i]; + var pointerToUse = objToCheck.group ? + this._normalizePointer(objToCheck.group, pointer) : pointer; + if (this._checkTarget(pointerToUse, objToCheck, pointer)) { + target = objects[i]; + if (target.subTargetCheck && target instanceof fabric.Group) { + subTarget = this._searchPossibleTargets(target._objects, pointer); + subTarget && this.targets.push(subTarget); + } + break; + } + } + return target; + }, + + /** + * Returns pointer coordinates without the effect of the viewport + * @param {Object} pointer with "x" and "y" number values + * @return {Object} object with "x" and "y" number values + */ + restorePointerVpt: function(pointer) { + return fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + }, + + /** + * Returns pointer coordinates relative to canvas. + * Can return coordinates with or without viewportTransform. + * ignoreZoom false gives back coordinates that represent + * the point clicked on canvas element. + * ignoreZoom true gives back coordinates after being processed + * by the viewportTransform ( sort of coordinates of what is displayed + * on the canvas where you are clicking. + * ignoreZoom true = HTMLElement coordinates relative to top,left + * ignoreZoom false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates + * compatible with the object.oCoords system. + * of the time. + * @param {Event} e + * @param {Boolean} ignoreZoom + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e, ignoreZoom) { + // return cached values if we are in the event processing chain + if (this._absolutePointer && !ignoreZoom) { + return this._absolutePointer; + } + if (this._pointer && ignoreZoom) { + return this._pointer; + } + + var pointer = getPointer(e), + upperCanvasEl = this.upperCanvasEl, + bounds = upperCanvasEl.getBoundingClientRect(), + boundsWidth = bounds.width || 0, + boundsHeight = bounds.height || 0, + cssScale; + + if (!boundsWidth || !boundsHeight ) { + if ('top' in bounds && 'bottom' in bounds) { + boundsHeight = Math.abs( bounds.top - bounds.bottom ); + } + if ('right' in bounds && 'left' in bounds) { + boundsWidth = Math.abs( bounds.right - bounds.left ); + } + } + + this.calcOffset(); + pointer.x = pointer.x - this._offset.left; + pointer.y = pointer.y - this._offset.top; + if (!ignoreZoom) { + pointer = this.restorePointerVpt(pointer); + } + + var retinaScaling = this.getRetinaScaling(); + if (retinaScaling !== 1) { + pointer.x /= retinaScaling; + pointer.y /= retinaScaling; + } + + if (boundsWidth === 0 || boundsHeight === 0) { + // If bounds are not available (i.e. not visible), do not apply scale. + cssScale = { width: 1, height: 1 }; + } + else { + cssScale = { + width: upperCanvasEl.width / boundsWidth, + height: upperCanvasEl.height / boundsHeight + }; + } + + return { + x: pointer.x * cssScale.width, + y: pointer.y * cssScale.height + }; + }, + + /** + * @private + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), + lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; + + // there is no need to create a new upperCanvas element if we have already one. + if (upperCanvasEl) { + upperCanvasEl.className = ''; + } + else { + upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl = upperCanvasEl; + } + fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); + + this.wrapperEl.appendChild(upperCanvasEl); + + this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); + this._applyCanvasStyle(upperCanvasEl); + this.contextTop = upperCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.width + 'px', + height: this.height + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @param {HTMLElement} element canvas element to apply styles on + */ + _applyCanvasStyle: function (element) { + var width = this.width || element.width, + height = this.height || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0, + 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', + '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Copy the entire inline style from one element (fromEl) to another (toEl) + * @private + * @param {Element} fromEl Element style is copied from + * @param {Element} toEl Element copied style is applied to + */ + _copyCanvasStyle: function (fromEl, toEl) { + toEl.style.cssText = fromEl.style.cssText; + }, + + /** + * Returns context of canvas where object selection is drawn + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Returns currently active object + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Returns an array with the current selected objects + * @return {fabric.Object} active object + */ + getActiveObjects: function () { + var active = this._activeObject; + if (active) { + if (active.type === 'activeSelection' && active._objects) { + return active._objects.slice(0); + } + else { + return [active]; + } + } + return []; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (obj === this._activeObject) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared', { target: obj }); + obj.fire('deselected'); + } + if (obj === this._hoveredTarget){ + this._hoveredTarget = null; + this._hoveredTargets = []; + } + this.callSuper('_onObjectRemoved', obj); + }, + + /** + * @private + * Compares the old activeObject with the current one and fires correct events + * @param {fabric.Object} obj old activeObject + */ + _fireSelectionEvents: function(oldObjects, e) { + var somethingChanged = false, objects = this.getActiveObjects(), + added = [], removed = []; + oldObjects.forEach(function(oldObject) { + if (objects.indexOf(oldObject) === -1) { + somethingChanged = true; + oldObject.fire('deselected', { + e: e, + target: oldObject + }); + removed.push(oldObject); + } + }); + objects.forEach(function(object) { + if (oldObjects.indexOf(object) === -1) { + somethingChanged = true; + object.fire('selected', { + e: e, + target: object + }); + added.push(object); + } + }); + if (oldObjects.length > 0 && objects.length > 0) { + somethingChanged && this.fire('selection:updated', { + e: e, + selected: added, + deselected: removed, + // added for backward compatibility + // deprecated + updated: added[0] || removed[0], + target: this._activeObject, + }); + } + else if (objects.length > 0) { + this.fire('selection:created', { + e: e, + selected: added, + target: this._activeObject, + }); + } + else if (oldObjects.length > 0) { + this.fire('selection:cleared', { + e: e, + deselected: removed, + }); + } + }, + + /** + * Sets given object as the only active object on canvas + * @param {fabric.Object} object Object to set as an active one + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + var currentActives = this.getActiveObjects(); + this._setActiveObject(object, e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * This is a private method for now. + * This is supposed to be equivalent to setActiveObject but without firing + * any event. There is commitment to have this stay this way. + * This is the functional part of setActiveObject. + * @private + * @param {Object} object to set as active + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {Boolean} true if the selection happened + */ + _setActiveObject: function(object, e) { + if (this._activeObject === object) { + return false; + } + if (!this._discardActiveObject(e, object)) { + return false; + } + if (object.onSelect({ e: e })) { + return false; + } + this._activeObject = object; + return true; + }, + + /** + * This is a private method for now. + * This is supposed to be equivalent to discardActiveObject but without firing + * any events. There is commitment to have this stay this way. + * This is the functional part of discardActiveObject. + * @param {Event} [e] Event (passed along when firing "object:deselected") + * @param {Object} object to set as active + * @return {Boolean} true if the selection happened + * @private + */ + _discardActiveObject: function(e, object) { + var obj = this._activeObject; + if (obj) { + // onDeselect return TRUE to cancel selection; + if (obj.onDeselect({ e: e, object: object })) { + return false; + } + this._activeObject = null; + } + return true; + }, + + /** + * Discards currently active object and fire events. If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parameter and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @param {event} e + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); + if (currentActives.length) { + this.fire('before:selection:cleared', { target: activeObject, e: e }); + } + this._discardActiveObject(e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * Clears a canvas element and removes all event listeners + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + var wrapper = this.wrapperEl; + this.removeListeners(); + wrapper.removeChild(this.upperCanvasEl); + wrapper.removeChild(this.lowerCanvasEl); + this.contextCache = null; + this.contextTop = null; + ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + if (wrapper.parentNode) { + wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); + } + delete this.wrapperEl; + fabric.StaticCanvas.prototype.dispose.call(this); + return this; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + // this.discardActiveGroup(); + this.discardActiveObject(); + this.clearContext(this.contextTop); + return this.callSuper('clear'); + }, + + /** + * Draws objects' controls (borders/controls) + * @param {CanvasRenderingContext2D} ctx Context to render controls on + */ + drawControls: function(ctx) { + var activeObject = this._activeObject; + + if (activeObject) { + activeObject._renderControls(ctx); + } + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + //If the object is part of the current selection group, it should + //be transformed appropriately + //i.e. it should be serialised as it would appear if the selection group + //were to be destroyed. + var originalProperties = this._realizeGroupTransformOnObject(instance), + object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); + //Undo the damage we did by changing all of its properties + this._unwindGroupTransformOnObject(instance, originalProperties); + return object; + }, + + /** + * Realises an object's group transformation on it + * @private + * @param {fabric.Object} [instance] the object to transform (gets mutated) + * @returns the original values of instance which were changed + */ + _realizeGroupTransformOnObject: function(instance) { + if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { + var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; + //Copy all the positionally relevant properties across now + var originalValues = {}; + layoutProps.forEach(function(prop) { + originalValues[prop] = instance[prop]; + }); + fabric.util.addTransformToObject(instance, this._activeObject.calcOwnMatrix()); + return originalValues; + } + else { + return null; + } + }, + + /** + * Restores the changed properties of instance + * @private + * @param {fabric.Object} [instance] the object to un-transform (gets mutated) + * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject + */ + _unwindGroupTransformOnObject: function(instance, originalValues) { + if (originalValues) { + instance.set(originalValues); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + //If the object is in a selection group, simulate what would happen to that + //object when the group is deselected + var originalProperties = this._realizeGroupTransformOnObject(instance); + this.callSuper('_setSVGObject', markup, instance, reviver); + this._unwindGroupTransformOnObject(instance, originalProperties); + }, + + setViewportTransform: function (vpt) { + if (this.renderOnAddRemove && this._activeObject && this._activeObject.isEditing) { + this._activeObject.clearContextTop(); + } + fabric.StaticCanvas.prototype.setViewportTransform.call(this, vpt); + } + }); + + // copying static properties manually to work around Opera's bug, + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } +})(); + + +(function() { + + var addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + RIGHT_CLICK = 3, MIDDLE_CLICK = 2, LEFT_CLICK = 1, + addEventOptions = { passive: false }; + + function checkClick(e, value) { + return e.button && (e.button === value - 1); + } + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Contains the id of the touch event that owns the fabric transform + * @type Number + * @private + */ + mainTouchId: null, + + /** + * Adds mouse listeners to canvas + * @private + */ + _initEventListeners: function () { + // in case we initialized the class twice. This should not happen normally + // but in some kind of applications where the canvas element may be changed + // this is a workaround to having double listeners. + this.removeListeners(); + this._bindEvents(); + this.addOrRemove(addListener, 'add'); + }, + + /** + * return an event prefix pointer or mouse. + * @private + */ + _getEventPrefix: function () { + return this.enablePointerEvents ? 'pointer' : 'mouse'; + }, + + addOrRemove: function(functor, eventjsFunctor) { + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + functor(fabric.window, 'resize', this._onResize); + functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + functor(canvasElement, eventTypePrefix + 'out', this._onMouseOut); + functor(canvasElement, eventTypePrefix + 'enter', this._onMouseEnter); + functor(canvasElement, 'wheel', this._onMouseWheel); + functor(canvasElement, 'contextmenu', this._onContextMenu); + functor(canvasElement, 'dblclick', this._onDoubleClick); + functor(canvasElement, 'dragover', this._onDragOver); + functor(canvasElement, 'dragenter', this._onDragEnter); + functor(canvasElement, 'dragleave', this._onDragLeave); + functor(canvasElement, 'drop', this._onDrop); + if (!this.enablePointerEvents) { + functor(canvasElement, 'touchstart', this._onTouchStart, addEventOptions); + } + if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { + eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); + eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); + eventjs[eventjsFunctor](canvasElement, 'orientation', this._onOrientationChange); + eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); + eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress); + } + }, + + /** + * Removes all event listeners + */ + removeListeners: function() { + this.addOrRemove(removeListener, 'remove'); + // if you dispose on a mouseDown, before mouse up, you need to clean document to... + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + }, + + /** + * @private + */ + _bindEvents: function() { + if (this.eventsBound) { + // for any reason we pass here twice we do not want to bind events twice. + return; + } + this._onMouseDown = this._onMouseDown.bind(this); + this._onTouchStart = this._onTouchStart.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onLongPress = this._onLongPress.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseEnter = this._onMouseEnter.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onDoubleClick = this._onDoubleClick.bind(this); + this._onDragOver = this._onDragOver.bind(this); + this._onDragEnter = this._simpleEventHandler.bind(this, 'dragenter'); + this._onDragLeave = this._simpleEventHandler.bind(this, 'dragleave'); + this._onDrop = this._simpleEventHandler.bind(this, 'drop'); + this.eventsBound = true; + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, self) { + this.__onTransformGesture && this.__onTransformGesture(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, self) { + this.__onDrag && this.__onDrag(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on wheel event + */ + _onMouseWheel: function(e) { + this.__onMouseWheel(e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseOut: function(e) { + var target = this._hoveredTarget; + this.fire('mouse:out', { target: target, e: e }); + this._hoveredTarget = null; + target && target.fire('mouseout', { e: e }); + + var _this = this; + this._hoveredTargets.forEach(function(_target){ + _this.fire('mouse:out', { target: target, e: e }); + _target && target.fire('mouseout', { e: e }); + }); + this._hoveredTargets = []; + + if (this._iTextInstances) { + this._iTextInstances.forEach(function(obj) { + if (obj.isEditing) { + obj.hiddenTextarea.focus(); + } + }); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mouseenter + */ + _onMouseEnter: function(e) { + // This find target and consequent 'mouse:over' is used to + // clear old instances on hovered target. + // calling findTarget has the side effect of killing target.__corner. + // as a short term fix we are not firing this if we are currently transforming. + // as a long term fix we need to separate the action of finding a target with the + // side effects we added to it. + if (!this._currentTransform && !this.findTarget(e)) { + this.fire('mouse:over', { target: null, e: e }); + this._hoveredTarget = null; + this._hoveredTargets = []; + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e, self) { + this.__onOrientationChange && this.__onOrientationChange(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e, self) { + this.__onShake && this.__onShake(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onLongPress: function(e, self) { + this.__onLongPress && this.__onLongPress(e, self); + }, + + /** + * prevent default to allow drop event to be fired + * @private + * @param {Event} [e] Event object fired on Event.js shake + */ + _onDragOver: function(e) { + e.preventDefault(); + var target = this._simpleEventHandler('dragover', e); + this._fireEnterLeaveEvents(target, e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onContextMenu: function (e) { + if (this.stopContextMenu) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onDoubleClick: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'dblclick'); + this._resetTransformEventData(e); + }, + + /** + * Return a the id of an event. + * returns either the pointerId or the identifier or 0 for the mouse event + * @private + * @param {Event} evt Event object + */ + getPointerId: function(evt) { + var changedTouches = evt.changedTouches; + + if (changedTouches) { + return changedTouches[0] && changedTouches[0].identifier; + } + + if (this.enablePointerEvents) { + return evt.pointerId; + } + + return -1; + }, + + /** + * Determines if an event has the id of the event that is considered main + * @private + * @param {evt} event Event object + */ + _isMainEvent: function(evt) { + if (evt.isPrimary === true) { + return true; + } + if (evt.isPrimary === false) { + return false; + } + if (evt.type === 'touchend' && evt.touches.length === 0) { + return true; + } + if (evt.changedTouches) { + return evt.changedTouches[0].identifier === this.mainTouchId; + } + return true; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchStart: function(e) { + e.preventDefault(); + if (this.mainTouchId === null) { + this.mainTouchId = this.getPointerId(e); + } + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + addListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + addListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + // Unbind mousedown to prevent double triggers from touch devices + removeListener(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown: function (e) { + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchEnd: function(e) { + if (e.touches.length > 0) { + // if there are still touches stop here + return; + } + this.__onMouseUp(e); + this._resetTransformEventData(); + this.mainTouchId = null; + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + var _this = this; + if (this._willAddMouseDown) { + clearTimeout(this._willAddMouseDown); + } + this._willAddMouseDown = setTimeout(function() { + // Wait 400ms before rebinding mousedown to prevent double triggers + // from touch devices + addListener(_this.upperCanvasEl, eventTypePrefix + 'down', _this._onMouseDown); + _this._willAddMouseDown = 0; + }, 400); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUp: function (e) { + this.__onMouseUp(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + if (this._isMainEvent(e)) { + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMove: function (e) { + !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); + this.__onMouseMove(e); + }, + + /** + * @private + */ + _onResize: function () { + this.calcOffset(); + }, + + /** + * Decides whether the canvas should be redrawn in mouseup and mousedown events. + * @private + * @param {Object} target + */ + _shouldRender: function(target) { + var activeObject = this._activeObject; + + if ( + !!activeObject !== !!target || + (activeObject && target && (activeObject !== target)) + ) { + // this covers: switch of target, from target to no target, selection of target + // multiSelection with key and mouse + return true; + } + else if (activeObject && activeObject.isEditing) { + // if we mouse up/down over a editing textbox a cursor change, + // there is no need to re render + return false; + } + return false; + }, + + /** + * Method that defines the actions when mouse is released on canvas. + * The method resets the currentTransform parameters, store the image corner + * position in the image object and render the canvas on top. + * @private + * @param {Event} e Event object fired on mouseup + */ + __onMouseUp: function (e) { + var target, transform = this._currentTransform, + groupSelector = this._groupSelector, shouldRender = false, + isClick = (!groupSelector || (groupSelector.left === 0 && groupSelector.top === 0)); + this._cacheTransformEventData(e); + target = this._target; + this._handleEvent(e, 'up:before'); + // if right/middle click just fire events and return + // target undefined will make the _handleEvent search the target + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'up', RIGHT_CLICK, isClick); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); + } + this._resetTransformEventData(); + return; + } + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._onMouseUpInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + if (transform) { + this._finalizeCurrentTransform(e); + shouldRender = transform.actionPerformed; + } + if (!isClick) { + var targetWasActive = target === this._activeObject; + this._maybeGroupObjects(e); + if (!shouldRender) { + shouldRender = ( + this._shouldRender(target) || + (!targetWasActive && target === this._activeObject) + ); + } + } + if (target) { + if (target.selectable && target !== this._activeObject && target.activeOn === 'up') { + this.setActiveObject(target, e); + shouldRender = true; + } + else { + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + var control = target.controls[corner], + mouseUpHandler = control && control.getMouseUpHandler(e, target, control); + if (mouseUpHandler) { + var pointer = this.getPointer(e); + mouseUpHandler(e, transform, pointer.x, pointer.y); + } + } + target.isMoving = false; + } + this._setCursorFromEvent(e, target); + this._handleEvent(e, 'up', LEFT_CLICK, isClick); + this._groupSelector = null; + this._currentTransform = null; + // reset the target information about which corner is selected + target && (target.__corner = 0); + if (shouldRender) { + this.requestRenderAll(); + } + else if (!isClick) { + this.renderTop(); + } + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @return {Fabric.Object} target return the the target found, for internal reasons. + */ + _simpleEventHandler: function(eventType, e) { + var target = this.findTarget(e), + targets = this.targets, + options = { + e: e, + target: target, + subTargets: targets, + }; + this.fire(eventType, options); + target && target.fire(eventType, options); + if (!targets) { + return target; + } + for (var i = 0; i < targets.length; i++) { + targets[i].fire(eventType, options); + } + return target; + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @param {fabric.Object} targetObj receiving event + * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right + * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. + */ + _handleEvent: function(e, eventType, button, isClick) { + var target = this._target, + targets = this.targets || [], + options = { + e: e, + target: target, + subTargets: targets, + button: button || LEFT_CLICK, + isClick: isClick || false, + pointer: this._pointer, + absolutePointer: this._absolutePointer, + transform: this._currentTransform + }; + if (eventType === 'up') { + options.currentTarget = this.findTarget(e); + options.currentSubTargets = this.targets; + } + this.fire('mouse:' + eventType, options); + target && target.fire('mouse' + eventType, options); + for (var i = 0; i < targets.length; i++) { + targets[i].fire('mouse' + eventType, options); + } + }, + + /** + * @private + * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event + */ + _finalizeCurrentTransform: function(e) { + + var transform = this._currentTransform, + target = transform.target, + eventName, + options = { + e: e, + target: target, + transform: transform, + action: transform.action, + }; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { + if (transform.actionPerformed) { + // this is not friendly to the new control api. + // is deprecated. + eventName = this._addEventOptions(options, transform); + this._fire(eventName, options); + } + this._fire('modified', options); + } + }, + + /** + * Mutate option object in order to add by property and give back the event name. + * @private + * @deprecated since 4.2.0 + * @param {Object} options to mutate + * @param {Object} transform to inspect action from + */ + _addEventOptions: function(options, transform) { + // we can probably add more details at low cost + // scale change, rotation changes, translation changes + var eventName, by; + switch (transform.action) { + case 'scaleX': + eventName = 'scaled'; + by = 'x'; + break; + case 'scaleY': + eventName = 'scaled'; + by = 'y'; + break; + case 'skewX': + eventName = 'skewed'; + by = 'x'; + break; + case 'skewY': + eventName = 'skewed'; + by = 'y'; + break; + case 'scale': + eventName = 'scaled'; + by = 'equally'; + break; + case 'rotate': + eventName = 'rotated'; + break; + case 'drag': + eventName = 'moved'; + break; + } + options.by = by; + return eventName; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDownInDrawingMode: function(e) { + this._isCurrentlyDrawing = true; + if (this.getActiveObject()) { + this.discardActiveObject(e).requestRenderAll(); + } + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); + this._handleEvent(e, 'down'); + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseMove(pointer, { e: e, pointer: pointer }); + } + this.setCursor(this.freeDrawingCursor); + this._handleEvent(e, 'move'); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + var pointer = this.getPointer(e); + this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); + this._handleEvent(e, 'up'); + }, + + /** + * Method that defines the actions when mouse is clicked on canvas. + * The method inits the currentTransform parameters and renders all the + * canvas so the current image can be placed on the top canvas and the rest + * in on the container one. + * @private + * @param {Event} e Event object fired on mousedown + */ + __onMouseDown: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'down:before'); + var target = this._target; + // if right click just fire events + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'down', RIGHT_CLICK); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'down', MIDDLE_CLICK); + } + return; + } + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) { + return; + } + + var pointer = this._pointer; + // save pointer for check in __onMouseUp event + this._previousPointer = pointer; + var shouldRender = this._shouldRender(target), + shouldGroup = this._shouldGroup(e, target); + if (this._shouldClearSelection(e, target)) { + this.discardActiveObject(e); + } + else if (shouldGroup) { + this._handleGrouping(e, target); + target = this._activeObject; + } + + if (this.selection && (!target || + (!target.selectable && !target.isEditing && target !== this._activeObject))) { + this._groupSelector = { + ex: this._absolutePointer.x, + ey: this._absolutePointer.y, + top: 0, + left: 0 + }; + } + + if (target) { + var alreadySelected = target === this._activeObject; + if (target.selectable && target.activeOn === 'down') { + this.setActiveObject(target, e); + } + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + target.__corner = corner; + if (target === this._activeObject && (corner || !shouldGroup)) { + this._setupCurrentTransform(e, target, alreadySelected); + var control = target.controls[corner], + pointer = this.getPointer(e), + mouseDownHandler = control && control.getMouseDownHandler(e, target, control); + if (mouseDownHandler) { + mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y); + } + } + } + this._handleEvent(e, 'down'); + // we must renderAll so that we update the visuals + (shouldRender || shouldGroup) && this.requestRenderAll(); + }, + + /** + * reset cache form common information needed during event processing + * @private + */ + _resetTransformEventData: function() { + this._target = null; + this._pointer = null; + this._absolutePointer = null; + }, + + /** + * Cache common information needed during event processing + * @private + * @param {Event} e Event object fired on event + */ + _cacheTransformEventData: function(e) { + // reset in order to avoid stale caching + this._resetTransformEventData(); + this._pointer = this.getPointer(e, true); + this._absolutePointer = this.restorePointerVpt(this._pointer); + this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null; + }, + + /** + * @private + */ + _beforeTransform: function(e) { + var t = this._currentTransform; + this.stateful && t.target.saveState(); + this.fire('before:transform', { + e: e, + transform: t, + }); + }, + + /** + * Method that defines the actions when mouse is hovering the canvas. + * The currentTransform parameter will define whether the user is rotating/scaling/translating + * an image or neither of them (only hovering). A group selection is also possible and would cancel + * all any other type of action. + * In case of an image transformation only the top canvas will be rendered. + * @private + * @param {Event} e Event object fired on mousemove + */ + __onMouseMove: function (e) { + this._handleEvent(e, 'move:before'); + this._cacheTransformEventData(e); + var target, pointer; + + if (this.isDrawingMode) { + this._onMouseMoveInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + pointer = this._absolutePointer; + + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; + + this.renderTop(); + } + else if (!this._currentTransform) { + target = this.findTarget(e) || null; + this._setCursorFromEvent(e, target); + this._fireOverOutEvents(target, e); + } + else { + this._transformObject(e); + } + this._handleEvent(e, 'move'); + this._resetTransformEventData(); + }, + + /** + * Manage the mouseout, mouseover events for the fabric object on the canvas + * @param {Fabric.Object} target the target where the target from the mousemove event + * @param {Event} e Event object fired on mousemove + * @private + */ + _fireOverOutEvents: function(target, e) { + var _hoveredTarget = this._hoveredTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _hoveredTarget, + evtOut: 'mouseout', + canvasEvtOut: 'mouse:out', + evtIn: 'mouseover', + canvasEvtIn: 'mouse:over', + }); + for (var i = 0; i < length; i++){ + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'mouseout', + evtIn: 'mouseover', + }); + } + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + }, + + /** + * Manage the dragEnter, dragLeave events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the onDrag event + * @param {Event} e Event object fired on ondrag + * @private + */ + _fireEnterLeaveEvents: function(target, e) { + var _draggedoverTarget = this._draggedoverTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _draggedoverTarget, + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + for (var i = 0; i < length; i++) { + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + } + this._draggedoverTarget = target; + }, + + /** + * Manage the synthetic in/out events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the supported events + * @param {Event} e Event object fired + * @param {Object} config configuration for the function to work + * @param {String} config.targetName property on the canvas where the old target is stored + * @param {String} [config.canvasEvtOut] name of the event to fire at canvas level for out + * @param {String} config.evtOut name of the event to fire for out + * @param {String} [config.canvasEvtIn] name of the event to fire at canvas level for in + * @param {String} config.evtIn name of the event to fire for in + * @private + */ + fireSyntheticInOutEvents: function(target, e, config) { + var inOpt, outOpt, oldTarget = config.oldTarget, outFires, inFires, + targetChanged = oldTarget !== target, canvasEvtIn = config.canvasEvtIn, canvasEvtOut = config.canvasEvtOut; + if (targetChanged) { + inOpt = { e: e, target: target, previousTarget: oldTarget }; + outOpt = { e: e, target: oldTarget, nextTarget: target }; + } + inFires = target && targetChanged; + outFires = oldTarget && targetChanged; + if (outFires) { + canvasEvtOut && this.fire(canvasEvtOut, outOpt); + oldTarget.fire(config.evtOut, outOpt); + } + if (inFires) { + canvasEvtIn && this.fire(canvasEvtIn, inOpt); + target.fire(config.evtIn, inOpt); + } + }, + + /** + * Method that defines actions when an Event Mouse Wheel + * @param {Event} e Event object fired on mouseup + */ + __onMouseWheel: function(e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'wheel'); + this._resetTransformEventData(); + }, + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + var pointer = this.getPointer(e), + transform = this._currentTransform; + + transform.reset = false; + transform.shiftKey = e.shiftKey; + transform.altKey = e[this.centeredKey]; + + this._performTransformAction(e, transform, pointer); + transform.actionPerformed && this.requestRenderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + action = transform.action, + actionPerformed = false, + actionHandler = transform.actionHandler; + // this object could be created from the function in the control handlers + + + if (actionHandler) { + actionPerformed = actionHandler(e, transform, x, y); + } + if (action === 'drag' && actionPerformed) { + transform.target.isMoving = true; + this.setCursor(transform.target.moveCursor || this.moveCursor); + } + transform.actionPerformed = transform.actionPerformed || actionPerformed; + }, + + /** + * @private + */ + _fire: fabric.controlsUtils.fireEvent, + + /** + * Sets the cursor depending on where the canvas is being hovered. + * Note: very buggy in Opera + * @param {Event} e Event object + * @param {Object} target Object that the mouse is hovering, if so. + */ + _setCursorFromEvent: function (e, target) { + if (!target) { + this.setCursor(this.defaultCursor); + return false; + } + var hoverCursor = target.hoverCursor || this.hoverCursor, + activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? + this._activeObject : null, + // only show proper corner when group selection is not active + corner = (!activeSelection || !activeSelection.contains(target)) + // here we call findTargetCorner always with undefined for the touch parameter. + // we assume that if you are using a cursor you do not need to interact with + // the bigger touch area. + && target._findTargetCorner(this.getPointer(e, true)); + + if (!corner) { + if (target.subTargetCheck){ + // hoverCursor should come from top-most subTarget, + // so we walk the array backwards + this.targets.concat().reverse().map(function(_target){ + hoverCursor = _target.hoverCursor || hoverCursor; + }); + } + this.setCursor(hoverCursor); + } + else { + this.setCursor(this.getCornerCursor(corner, target, e)); + } + }, + + /** + * @private + */ + getCornerCursor: function(corner, target, e) { + var control = target.controls[corner]; + return control.cursorStyleHandler(e, control, target); + } + }); +})(); + + +(function() { + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this._activeObject; + return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + var activeObject = this._activeObject; + // avoid multi select when shift click on a corner + if (activeObject.__corner) { + return; + } + if (target === activeObject) { + // if it's a group, find target again, using activeGroup objects + target = this.findTarget(e, true); + // if even object is not found or we are on activeObjectCorner, bail out + if (!target || !target.selectable) { + return; + } + } + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(target, e); + } + else { + this._createActiveSelection(target, e); + } + }, + + /** + * @private + */ + _updateActiveSelection: function(target, e) { + var activeSelection = this._activeObject, + currentActiveObjects = activeSelection._objects.slice(0); + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + if (activeSelection.size() === 1) { + // activate last remaining object + this._setActiveObject(activeSelection.item(0), e); + } + } + else { + activeSelection.addWithUpdate(target); + this._hoveredTarget = activeSelection; + this._hoveredTargets = this.targets.concat(); + } + this._fireSelectionEvents(currentActiveObjects, e); + }, + + /** + * @private + */ + _createActiveSelection: function(target, e) { + var currentActives = this.getActiveObjects(), group = this._createGroup(target); + this._hoveredTarget = group; + // ISSUE 4115: should we consider subTargets here? + // this._hoveredTargets = []; + // this._hoveredTargets = this.targets.concat(); + this._setActiveObject(group, e); + this._fireSelectionEvents(currentActives, e); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + var objects = this._objects, + isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), + groupObjects = isActiveLower + ? [this._activeObject, target] + : [target, this._activeObject]; + this._activeObject.isEditing && this._activeObject.exitEditing(); + return new fabric.ActiveSelection(groupObjects, { + canvas: this + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(e), + aGroup; + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + aGroup = new fabric.ActiveSelection(group.reverse(), { + canvas: this + }); + this.setActiveObject(aGroup, e); + } + }, + + /** + * @private + */ + _collectObjects: function(e) { + var group = [], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + allowIntersect = !this.selectionFullyContained, + isClick = x1 === x2 && y1 === y2; + // we iterate reverse order to collect top first in case of click. + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) { + continue; + } + + if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2, true)) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2, true) || + (allowIntersect && currentObject.containsPoint(selectionX1Y1, null, true)) || + (allowIntersect && currentObject.containsPoint(selectionX2Y2, null, true)) + ) { + group.push(currentObject); + // only add one object if it's a click + if (isClick) { + break; + } + } + } + + if (group.length > 1) { + group = group.filter(function(object) { + return !object.onSelect({ e: e }); + }); + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + this.setCursor(this.defaultCursor); + // clear selection and current transformation + this._groupSelector = null; + } + }); + +})(); + + +(function () { + fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately + * @param {Object} [options] Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @example Generate jpeg dataURL with lower quality + * var dataURL = canvas.toDataURL({ + * format: 'jpeg', + * quality: 0.8 + * }); + * @example Generate cropped png dataURL (clipping of canvas) + * var dataURL = canvas.toDataURL({ + * format: 'png', + * left: 100, + * top: 100, + * width: 200, + * height: 200 + * }); + * @example Generate double scaled png dataURL + * var dataURL = canvas.toDataURL({ + * format: 'png', + * multiplier: 2 + * }); + */ + toDataURL: function (options) { + options || (options = { }); + + var format = options.format || 'png', + quality = options.quality || 1, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), + canvasEl = this.toCanvasElement(multiplier, options); + return fabric.util.toDataURL(canvasEl, format, quality); + }, + + /** + * Create a new HTMLCanvas element painted with the current canvas content. + * No need to resize the actual one or repaint it. + * Will transfer object ownership to a new canvas, paint it, and set everything back. + * This is an intermediary step used to get to a dataUrl but also it is useful to + * create quick image copies of a canvas without passing for the dataUrl string + * @param {Number} [multiplier] a zoom factor. + * @param {Object} [cropping] Cropping informations + * @param {Number} [cropping.left] Cropping left offset. + * @param {Number} [cropping.top] Cropping top offset. + * @param {Number} [cropping.width] Cropping width. + * @param {Number} [cropping.height] Cropping height. + */ + toCanvasElement: function(multiplier, cropping) { + multiplier = multiplier || 1; + cropping = cropping || { }; + var scaledWidth = (cropping.width || this.width) * multiplier, + scaledHeight = (cropping.height || this.height) * multiplier, + zoom = this.getZoom(), + originalWidth = this.width, + originalHeight = this.height, + newZoom = zoom * multiplier, + vp = this.viewportTransform, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, + originalInteractive = this.interactive, + newVp = [newZoom, 0, 0, newZoom, translateX, translateY], + originalRetina = this.enableRetinaScaling, + canvasEl = fabric.util.createCanvasElement(), + originalContextTop = this.contextTop; + canvasEl.width = scaledWidth; + canvasEl.height = scaledHeight; + this.contextTop = null; + this.enableRetinaScaling = false; + this.interactive = false; + this.viewportTransform = newVp; + this.width = scaledWidth; + this.height = scaledHeight; + this.calcViewportBoundaries(); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); + this.viewportTransform = vp; + this.width = originalWidth; + this.height = originalHeight; + this.calcViewportBoundaries(); + this.interactive = originalInteractive; + this.enableRetinaScaling = originalRetina; + this.contextTop = originalContextTop; + return canvasEl; + }, + }); + +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + /** + * Populates canvas with data from the specified JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} + * @example loadFromJSON + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * @example loadFromJSON with reviver + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * // `o` = json object + * // `object` = fabric.Object instance + * // ... do some stuff ... + * }); + */ + loadFromJSON: function (json, callback, reviver) { + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : fabric.util.object.clone(json); + + var _this = this, + clipPath = serialized.clipPath, + renderOnAddRemove = this.renderOnAddRemove; + + this.renderOnAddRemove = false; + + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); + _this._setBgOverlay(serialized, function () { + if (clipPath) { + _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { + _this.clipPath = enlivenedCanvasClip[0]; + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + }); + } + else { + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + } + }); + }, reviver); + return this; + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Array} restored canvas objects + * @param {Function} cached renderOnAddRemove callback + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { + var _this = this; + enlivenedObjects.forEach(function(obj, index) { + // we splice the array just in case some custom classes restored from JSON + // will add more object to canvas at canvas init. + _this.insertAt(obj, index); + }); + this.renderOnAddRemove = renderOnAddRemove; + // remove parts i cannot set as options + delete serialized.objects; + delete serialized.backgroundImage; + delete serialized.overlayImage; + delete serialized.background; + delete serialized.overlay; + // this._initOptions does too many things to just + // call it. Normally loading an Object from JSON + // create the Object instance. Here the Canvas is + // already an instance and we are just loading things over it + this._setOptions(serialized); + this.renderAll(); + callback && callback(); + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + }, + + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + if (!objects || objects.length === 0) { + callback && callback([]); + return; + } + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + */ + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.util.createCanvasElement(); + + el.width = this.width; + el.height = this.height; + + var clone = new fabric.Canvas(el); + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + degreesToRadians = fabric.util.degreesToRadians, + objectCaching = !fabric.isLikelyNode, + ALIASING_LIMIT = 2; + + if (fabric.Object) { + return; + } + + /** + * Root object class from which all 2d shape classes inherit from + * @class fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects} + * @see {@link fabric.Object#initialize} for constructor definition + * + * @fires added + * @fires removed + * + * @fires selected + * @fires deselected + * @fires modified + * @fires modified + * @fires moved + * @fires scaled + * @fires rotated + * @fires skewed + * + * @fires rotating + * @fires scaling + * @fires moving + * @fires skewing + * + * @fires mousedown + * @fires mouseup + * @fires mouseover + * @fires mouseout + * @fires mousewheel + * @fires mousedblclick + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + */ + fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc.). + * Note that this property is meant to be read-only and not meant to be modified. + * If you modify, certain parts of Fabric (such as JSON loading) won't work correctly. + * @type String + * @default + */ + type: 'object', + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originX: 'left', + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originY: 'top', + + /** + * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} + * @type Number + * @default + */ + top: 0, + + /** + * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} + * @type Number + * @default + */ + left: 0, + + /** + * Object width + * @type Number + * @default + */ + width: 0, + + /** + * Object height + * @type Number + * @default + */ + height: 0, + + /** + * Object scale factor (horizontal) + * @type Number + * @default + */ + scaleX: 1, + + /** + * Object scale factor (vertical) + * @type Number + * @default + */ + scaleY: 1, + + /** + * When true, an object is rendered as flipped horizontally + * @type Boolean + * @default + */ + flipX: false, + + /** + * When true, an object is rendered as flipped vertically + * @type Boolean + * @default + */ + flipY: false, + + /** + * Opacity of an object + * @type Number + * @default + */ + opacity: 1, + + /** + * Angle of rotation of an object (in degrees) + * @type Number + * @default + */ + angle: 0, + + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default + */ + skewX: 0, + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default + */ + skewY: 0, + + /** + * Size of object's controlling corners (in pixels) + * @type Number + * @default + */ + cornerSize: 13, + + /** + * Size of object's controlling corners when touch interaction is detected + * @type Number + * @default + */ + touchCornerSize: 24, + + /** + * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) + * @type Boolean + * @default + */ + transparentCorners: true, + + /** + * Default cursor value used when hovering over this object on canvas + * @type String + * @default + */ + hoverCursor: null, + + /** + * Default cursor value used when moving this object on canvas + * @type String + * @default + */ + moveCursor: null, + + /** + * Padding between object and its controlling borders (in pixels) + * @type Number + * @default + */ + padding: 0, + + /** + * Color of controlling borders of an object (when it's active) + * @type String + * @default + */ + borderColor: 'rgb(178,204,255)', + + /** + * Array specifying dash pattern of an object's borders (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + borderDashArray: null, + + /** + * Color of controlling corners of an object (when it's active) + * @type String + * @default + */ + cornerColor: 'rgb(178,204,255)', + + /** + * Color of controlling corners of an object (when it's active and transparentCorners false) + * @since 1.6.2 + * @type String + * @default + */ + cornerStrokeColor: null, + + /** + * Specify style of control, 'rect' or 'circle' + * @since 1.6.2 + * @type String + */ + cornerStyle: 'rect', + + /** + * Array specifying dash pattern of an object's control (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + cornerDashArray: null, + + /** + * When true, this object will use center point as the origin of transformation + * when being scaled via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, this object will use center point as the origin of transformation + * when being rotated via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: true, + + /** + * Color of object's fill + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + fill: 'rgb(0,0,0)', + + /** + * Fill rule used to fill an object + * accepted values are nonzero, evenodd + * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) + * @type String + * @default + */ + fillRule: 'nonzero', + + /** + * Composite rule used for canvas globalCompositeOperation + * @type String + * @default + */ + globalCompositeOperation: 'source-over', + + /** + * Background color of an object. + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + backgroundColor: '', + + /** + * Selection Background color of an object. colored layer behind the object when it is active. + * does not mix good with globalCompositeOperation methods. + * @type String + * @default + */ + selectionBackgroundColor: '', + + /** + * When defined, an object is rendered via stroke and this property specifies its color + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + stroke: null, + + /** + * Width of a stroke used to render this object + * @type Number + * @default + */ + strokeWidth: 1, + + /** + * Array specifying dash pattern of an object's stroke (stroke must be defined) + * @type Array + */ + strokeDashArray: null, + + /** + * Line offset of an object's stroke + * @type Number + * @default + */ + strokeDashOffset: 0, + + /** + * Line endings style of an object's stroke (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'butt', + + /** + * Corner style of an object's stroke (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'miter', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke + * @type Number + * @default + */ + strokeMiterLimit: 4, + + /** + * Shadow object representing shadow of this shape + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Opacity of object's controlling borders when object is active and moving + * @type Number + * @default + */ + borderOpacityWhenMoving: 0.4, + + /** + * Scale factor of object's controlling borders + * bigger number will make a thicker border + * border is 1, so this is basically a border thickness + * since there is no way to change the border itself. + * @type Number + * @default + */ + borderScaleFactor: 1, + + /** + * Minimum allowed scale value of an object + * @type Number + * @default + */ + minScaleLimit: 0, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). + * But events still fire on it. + * @type Boolean + * @default + */ + selectable: true, + + /** + * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 + * @type Boolean + * @default + */ + evented: true, + + /** + * When set to `false`, an object is not rendered on canvas + * @type Boolean + * @default + */ + visible: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @type Boolean + * @default + */ + hasControls: true, + + /** + * When set to `false`, object's controlling borders are not rendered + * @type Boolean + * @default + */ + hasBorders: true, + + /** + * When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * When `false`, default object's values are not included in its serialization + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `true`, object horizontal movement is locked + * @type Boolean + * @default + */ + lockMovementX: false, + + /** + * When `true`, object vertical movement is locked + * @type Boolean + * @default + */ + lockMovementY: false, + + /** + * When `true`, object rotation is locked + * @type Boolean + * @default + */ + lockRotation: false, + + /** + * When `true`, object horizontal scaling is locked + * @type Boolean + * @default + */ + lockScalingX: false, + + /** + * When `true`, object vertical scaling is locked + * @type Boolean + * @default + */ + lockScalingY: false, + + /** + * When `true`, object horizontal skewing is locked + * @type Boolean + * @default + */ + lockSkewingX: false, + + /** + * When `true`, object vertical skewing is locked + * @type Boolean + * @default + */ + lockSkewingY: false, + + /** + * When `true`, object cannot be flipped by scaling into negative values + * @type Boolean + * @default + */ + lockScalingFlip: false, + + /** + * When `true`, object is not exported in OBJECT/JSON + * @since 1.6.3 + * @type Boolean + * @default + */ + excludeFromExport: false, + + /** + * When `true`, object is cached on an additional canvas. + * When `false`, object is not cached unless necessary ( clipPath ) + * default to true + * @since 1.7.0 + * @type Boolean + * @default true + */ + objectCaching: objectCaching, + + /** + * When `true`, object properties are checked for cache invalidation. In some particular + * situation you may want this to be disabled ( spray brush, very big, groups) + * or if your application does not allow you to modify properties for groups child you want + * to disable it for groups. + * default to false + * since 1.7.0 + * @type Boolean + * @default false + */ + statefullCache: false, + + /** + * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled + * too much and will be redrawn with correct details at the end of scaling. + * this setting is performance and application dependant. + * default to true + * since 1.7.0 + * @type Boolean + * @default true + */ + noScaleCache: true, + + /** + * When `false`, the stoke width will scale with the object. + * When `true`, the stroke will always match the exact pixel size entered for stroke width. + * default to false + * @since 2.6.0 + * @type Boolean + * @default false + * @type Boolean + * @default false + */ + strokeUniform: false, + + /** + * When set to `true`, object's cache will be rerendered next render call. + * since 1.7.0 + * @type Boolean + * @default true + */ + dirty: true, + + /** + * keeps the value of the last hovered corner during mouse move. + * 0 is no corner, or 'mt', 'ml', 'mtr' etc.. + * It should be private, but there is no harm in using it as + * a read-only property. + * @type number|string|any + * @default 0 + */ + __corner: 0, + + /** + * Determines if the fill or the stroke is drawn first (one of "fill" or "stroke") + * @type String + * @default + */ + paintFirst: 'fill', + + /** + * When 'down', object is set to active on mousedown/touchstart + * When 'up', object is set to active on mouseup/touchend + * Experimental. Let's see if this breaks anything before supporting officially + * @private + * since 4.4.0 + * @type String + * @default 'down' + */ + activeOn: 'down', + + /** + * List of properties to consider when checking if state + * of an object is changed (fabric.Object#hasStateChanged) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + + 'stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit ' + + 'angle opacity fill globalCompositeOperation shadow visible backgroundColor ' + + 'skewX skewY fillRule paintFirst clipPath strokeUniform' + ).split(' '), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: ( + 'fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform' + + ' strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath' + ).split(' '), + + /** + * List of properties to consider for animating colors. + * @type Array + */ + colorProperties: ( + 'fill stroke backgroundColor' + ).split(' '), + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the object has rendered, and the context is placed in the center + * of the object cacheCanvas. + * If you want 0,0 of a clipPath to align with an object center, use clipPath.originX/Y to 'center' + * @type fabric.Object + */ + clipPath: undefined, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will make the object clip to the outside of the clipPath + * since 2.4.0 + * @type boolean + * @default false + */ + inverted: false, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will have its top and left relative to canvas, and will + * not be influenced by the object transform. This will make the clipPath relative + * to the canvas, but clipping just a particular object. + * WARNING this is beta, this feature may change or be renamed. + * since 2.4.0 + * @type boolean + * @default false + */ + absolutePositioned: false, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Create a the canvas used to keep the cached copy of the object + * @private + */ + _createCacheCanvas: function() { + this._cacheProperties = {}; + this._cacheCanvas = fabric.util.createCanvasElement(); + this._cacheContext = this._cacheCanvas.getContext('2d'); + this._updateCacheCanvas(); + // if canvas gets created, is empty, so dirty. + this.dirty = true; + }, + + /** + * Limit the cache dimensions so that X * Y do not cross fabric.perfLimitSizeTotal + * and each side do not cross fabric.cacheSideLimit + * those numbers are configurable so that you can get as much detail as you want + * making bargain with performances. + * @param {Object} dims + * @param {Object} dims.width width of canvas + * @param {Object} dims.height height of canvas + * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _limitCacheSize: function(dims) { + var perfLimitSizeTotal = fabric.perfLimitSizeTotal, + width = dims.width, height = dims.height, + max = fabric.maxCacheSideLimit, min = fabric.minCacheSideLimit; + if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { + if (width < min) { + dims.width = min; + } + if (height < min) { + dims.height = min; + } + return dims; + } + var ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal), + capValue = fabric.util.capValue, + x = capValue(min, limitedDims.x, max), + y = capValue(min, limitedDims.y, max); + if (width > x) { + dims.zoomX /= width / x; + dims.width = x; + dims.capped = true; + } + if (height > y) { + dims.zoomY /= height / y; + dims.height = y; + dims.capped = true; + } + return dims; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @return {Object}.x width of object to be cached + * @return {Object}.y height of object to be cached + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var objectScale = this.getTotalObjectScaling(), + // caculate dimensions without skewing + dim = this._getTransformedDimensions(0, 0), + neededX = dim.x * objectScale.scaleX / this.scaleX, + neededY = dim.y * objectScale.scaleY / this.scaleY; + return { + // for sure this ALIASING_LIMIT is slightly creating problem + // in situation in which the cache canvas gets an upper limit + // also objectScale contains already scaleX and scaleY + width: neededX + ALIASING_LIMIT, + height: neededY + ALIASING_LIMIT, + zoomX: objectScale.scaleX, + zoomY: objectScale.scaleY, + x: neededX, + y: neededY + }; + }, + + /** + * Update width and height of the canvas for cache + * returns true or false if canvas needed resize. + * @private + * @return {Boolean} true if the canvas has been resized + */ + _updateCacheCanvas: function() { + var targetCanvas = this.canvas; + if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) { + var target = targetCanvas._currentTransform.target, + action = targetCanvas._currentTransform.action; + if (this === target && action.slice && action.slice(0, 5) === 'scale') { + return false; + } + } + var canvas = this._cacheCanvas, + dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + minCacheSize = fabric.minCacheSideLimit, + width = dims.width, height = dims.height, drawingWidth, drawingHeight, + zoomX = dims.zoomX, zoomY = dims.zoomY, + dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight, + zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY, + shouldRedraw = dimensionsChanged || zoomChanged, + additionalWidth = 0, additionalHeight = 0, shouldResizeCanvas = false; + if (dimensionsChanged) { + var canvasWidth = this._cacheCanvas.width, + canvasHeight = this._cacheCanvas.height, + sizeGrowing = width > canvasWidth || height > canvasHeight, + sizeShrinking = (width < canvasWidth * 0.9 || height < canvasHeight * 0.9) && + canvasWidth > minCacheSize && canvasHeight > minCacheSize; + shouldResizeCanvas = sizeGrowing || sizeShrinking; + if (sizeGrowing && !dims.capped && (width > minCacheSize || height > minCacheSize)) { + additionalWidth = width * 0.1; + additionalHeight = height * 0.1; + } + } + if (this instanceof fabric.Text && this.path) { + shouldRedraw = true; + shouldResizeCanvas = true; + additionalWidth += this.getHeightOfLine(0) * this.zoomX; + additionalHeight += this.getHeightOfLine(0) * this.zoomY; + } + if (shouldRedraw) { + if (shouldResizeCanvas) { + canvas.width = Math.ceil(width + additionalWidth); + canvas.height = Math.ceil(height + additionalHeight); + } + else { + this._cacheContext.setTransform(1, 0, 0, 1, 0, 0); + this._cacheContext.clearRect(0, 0, canvas.width, canvas.height); + } + drawingWidth = dims.x / 2; + drawingHeight = dims.y / 2; + this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; + this.cacheTranslationY = Math.round(canvas.height / 2 - drawingHeight) + drawingHeight; + this.cacheWidth = width; + this.cacheHeight = height; + this._cacheContext.translate(this.cacheTranslationX, this.cacheTranslationY); + this._cacheContext.scale(zoomX, zoomY); + this.zoomX = zoomX; + this.zoomY = zoomY; + return true; + } + return false; + }, + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + this._setOptions(options); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); + }, + + /** + * Transforms context when rendering an object + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function(ctx) { + var needFullTransform = (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); + var m = this.calcTransformMatrix(!needFullTransform); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + }, + + /** + * Returns an object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + + object = { + type: this.type, + version: fabric.version, + originX: this.originX, + originY: this.originY, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, + strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), + strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray, + strokeLineCap: this.strokeLineCap, + strokeDashOffset: this.strokeDashOffset, + strokeLineJoin: this.strokeLineJoin, + strokeUniform: this.strokeUniform, + strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.angle, NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, + visible: this.visible, + backgroundColor: this.backgroundColor, + fillRule: this.fillRule, + paintFirst: this.paintFirst, + globalCompositeOperation: this.globalCompositeOperation, + skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS), + }; + + if (this.clipPath && !this.clipPath.excludeFromExport) { + object.clipPath = this.clipPath.toObject(propertiesToInclude); + object.clipPath.inverted = this.clipPath.inverted; + object.clipPath.absolutePositioned = this.clipPath.absolutePositioned; + } + + fabric.util.populateWithProperties(this, object, propertiesToInclude); + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + // will be overwritten by subclasses + return this.toObject(propertiesToInclude); + }, + + /** + * @private + * @param {Object} object + */ + _removeDefaultValues: function(object) { + var prototype = fabric.util.getKlass(object.type).prototype, + stateProperties = prototype.stateProperties; + stateProperties.forEach(function(prop) { + if (prop === 'left' || prop === 'top') { + return; + } + if (object[prop] === prototype[prop]) { + delete object[prop]; + } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + + // basically a check for [] === [] + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + delete object[prop]; + } + }); + + return object; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Return the object scale factor counting also the group scaling + * @return {Object} object with scaleX and scaleY properties + */ + getObjectScaling: function() { + // if the object is a top level one, on the canvas, we go for simple aritmetic + // otherwise the complex method with angles will return approximations and decimals + // and will likely kill the cache when not needed + // https://github.com/fabricjs/fabric.js/issues/7157 + if (!this.group) { + return { + scaleX: this.scaleX, + scaleY: this.scaleY, + }; + } + // if we are inside a group total zoom calculation is complex, we defer to generic matrices + var options = fabric.util.qrDecompose(this.calcTransformMatrix()); + return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; + }, + + /** + * Return the object scale factor counting also the group scaling, zoom and retina + * @return {Object} object with scaleX and scaleY properties + */ + getTotalObjectScaling: function() { + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + if (this.canvas) { + var zoom = this.canvas.getZoom(); + var retina = this.canvas.getRetinaScaling(); + scaleX *= zoom * retina; + scaleY *= zoom * retina; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + + /** + * Return the object opacity counting also the group property + * @return {Number} + */ + getObjectOpacity: function() { + var opacity = this.opacity; + if (this.group) { + opacity *= this.group.getObjectOpacity(); + } + return opacity; + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Object} thisArg + */ + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'), + isChanged = this[key] !== value, groupNeedsUpdate = false; + + if (shouldConstrainValue) { + value = this._constrainScale(value); + } + if (key === 'scaleX' && value < 0) { + this.flipX = !this.flipX; + value *= -1; + } + else if (key === 'scaleY' && value < 0) { + this.flipY = !this.flipY; + value *= -1; + } + else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { + value = new fabric.Shadow(value); + } + else if (key === 'dirty' && this.group) { + this.group.set('dirty', value); + } + + this[key] = value; + + if (isChanged) { + groupNeedsUpdate = this.group && this.group.isOnACache(); + if (this.cacheProperties.indexOf(key) > -1) { + this.dirty = true; + groupNeedsUpdate && this.group.set('dirty', true); + } + else if (groupNeedsUpdate && this.stateProperties.indexOf(key) > -1) { + this.group.set('dirty', true); + } + } + return this; + }, + + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf fabric.Object.prototype + * @return {Array} + */ + getViewportTransform: function() { + if (this.canvas && this.canvas.viewportTransform) { + return this.canvas.viewportTransform; + } + return fabric.iMatrix.concat(); + }, + + /* + * @private + * return if the object would be visible in rendering + * @memberOf fabric.Object.prototype + * @return {Boolean} + */ + isNotVisible: function() { + return this.opacity === 0 || + (!this.width && !this.height && this.strokeWidth === 0) || + !this.visible; + }, + + /** + * Renders an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if width/height are zeros or object is not visible + if (this.isNotVisible()) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + ctx.save(); + this._setupCompositeOperation(ctx); + this.drawSelectionBackground(ctx); + this.transform(ctx); + this._setOpacity(ctx); + this._setShadow(ctx, this); + if (this.shouldCache()) { + this.renderCache(); + this.drawCacheOnCanvas(ctx); + } + else { + this._removeCacheCanvas(); + this.dirty = false; + this.drawObject(ctx); + if (this.objectCaching && this.statefullCache) { + this.saveState({ propertySet: 'cacheProperties' }); + } + } + ctx.restore(); + }, + + renderCache: function(options) { + options = options || {}; + if (!this._cacheCanvas) { + this._createCacheCanvas(); + } + if (this.isCacheDirty()) { + this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, options.forClipping); + this.dirty = false; + } + }, + + /** + * Remove cacheCanvas and its dimensions from the objects + */ + _removeCacheCanvas: function() { + this._cacheCanvas = null; + this.cacheWidth = 0; + this.cacheHeight = 0; + }, + + /** + * return true if the object will draw a stroke + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an approximation and be fast. + * wrote to avoid extra caching, it has to return true when stroke happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the stroke is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasStroke: function() { + return this.stroke && this.stroke !== 'transparent' && this.strokeWidth !== 0; + }, + + /** + * return true if the object will draw a fill + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an approximation and be fast. + * wrote to avoid extra caching, it has to return true when fill happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the fill is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasFill: function() { + return this.fill && this.fill !== 'transparent'; + }, + + /** + * When set to `true`, force the object to have its own cache, even if it is inside a group + * it may be needed when your object behave in a particular way on the cache and always needs + * its own isolated canvas to render correctly. + * Created to be overridden + * since 1.7.12 + * @returns Boolean + */ + needsItsOwnCache: function() { + if (this.paintFirst === 'stroke' && + this.hasFill() && this.hasStroke() && typeof this.shadow === 'object') { + return true; + } + if (this.clipPath) { + return true; + } + return false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * Read as: cache if is needed, or if the feature is enabled but we are not already caching. + * @return {Boolean} + */ + shouldCache: function() { + this.ownCaching = this.needsItsOwnCache() || ( + this.objectCaching && + (!this.group || !this.group.isOnACache()) + ); + return this.ownCaching; + }, + + /** + * Check if this object or a child object will cast a shadow + * used by Group.shouldCache to know if child has a shadow recursively + * @return {Boolean} + */ + willDrawShadow: function() { + return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); + }, + + /** + * Execute the drawing operation for an object clipPath + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCache: function(ctx) { + var path = this.clipPath; + ctx.save(); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4 + if (path.inverted) { + ctx.globalCompositeOperation = 'destination-out'; + } + else { + ctx.globalCompositeOperation = 'destination-in'; + } + //ctx.scale(1 / 2, 1 / 2); + if (path.absolutePositioned) { + var m = fabric.util.invertTransform(this.calcTransformMatrix()); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx, forClipping) { + var originalFill = this.fill, originalStroke = this.stroke; + if (forClipping) { + this.fill = 'black'; + this.stroke = ''; + this._setClippingProperties(ctx); + } + else { + this._renderBackground(ctx); + } + this._render(ctx); + this._drawClipPath(ctx); + this.fill = originalFill; + this.stroke = originalStroke; + }, + + _drawClipPath: function(ctx) { + var path = this.clipPath; + if (!path) { return; } + // needed to setup a couple of variables + // path canvas gets overridden with this one. + // TODO find a better solution? + path.canvas = this.canvas; + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCache(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + ctx.scale(1 / this.zoomX, 1 / this.zoomY); + ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); + }, + + /** + * Check if cache is dirty + * @param {Boolean} skipCanvas skip canvas checks because this object is painted + * on parent canvas. + */ + isCacheDirty: function(skipCanvas) { + if (this.isNotVisible()) { + return false; + } + if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { + // in this case the context is already cleared. + return true; + } + else { + if (this.dirty || + (this.clipPath && this.clipPath.absolutePositioned) || + (this.statefullCache && this.hasStateChanged('cacheProperties')) + ) { + if (this._cacheCanvas && !skipCanvas) { + var width = this.cacheWidth / this.zoomX; + var height = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-width / 2, -height / 2, width, height); + } + return true; + } + } + return false; + }, + + /** + * Draws a background for the object big as its untransformed dimensions + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (!this.backgroundColor) { + return; + } + var dim = this._getNonTransformedDimensions(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + -dim.x / 2, + -dim.y / 2, + dim.x, + dim.y + ); + // if there is background color no other shadows + // should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setOpacity: function(ctx) { + if (this.group && !this.group._transformDone) { + ctx.globalAlpha = this.getObjectOpacity(); + } + else { + ctx.globalAlpha *= this.opacity; + } + }, + + _setStrokeStyles: function(ctx, decl) { + var stroke = decl.stroke; + if (stroke) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = decl.strokeLineCap; + ctx.lineDashOffset = decl.strokeDashOffset; + ctx.lineJoin = decl.strokeLineJoin; + ctx.miterLimit = decl.strokeMiterLimit; + if (stroke.toLive) { + if (stroke.gradientUnits === 'percentage' || stroke.gradientTransform || stroke.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + this._applyPatternForTransformedGradient(ctx, stroke); + } + else { + // is a simple gradient or pattern + ctx.strokeStyle = stroke.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, stroke); + } + } + else { + // is a color + ctx.strokeStyle = decl.stroke; + } + } + }, + + _setFillStyles: function(ctx, decl) { + var fill = decl.fill; + if (fill) { + if (fill.toLive) { + ctx.fillStyle = fill.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, decl.fill); + } + else { + ctx.fillStyle = fill; + } + } + }, + + _setClippingProperties: function(ctx) { + ctx.globalAlpha = 1; + ctx.strokeStyle = 'transparent'; + ctx.fillStyle = '#000000'; + }, + + /** + * @private + * Sets line dash + * @param {CanvasRenderingContext2D} ctx Context to set the dash line on + * @param {Array} dashArray array representing dashes + */ + _setLineDash: function(ctx, dashArray) { + if (!dashArray || dashArray.length === 0) { + return; + } + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & dashArray.length) { + dashArray.push.apply(dashArray, dashArray); + } + ctx.setLineDash(dashArray); + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + */ + _renderControls: function(ctx, styleOverride) { + var vpt = this.getViewportTransform(), + matrix = this.calcTransformMatrix(), + options, drawBorders, drawControls; + styleOverride = styleOverride || { }; + drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders; + drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls; + matrix = fabric.util.multiplyTransformMatrices(vpt, matrix); + options = fabric.util.qrDecompose(matrix); + ctx.save(); + ctx.translate(options.translateX, options.translateY); + ctx.lineWidth = 1 * this.borderScaleFactor; + if (!this.group) { + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + } + ctx.rotate(degreesToRadians(options.angle)); + if (styleOverride.forActiveSelection || this.group) { + drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); + } + else { + drawBorders && this.drawBorders(ctx, styleOverride); + } + drawControls && this.drawControls(ctx, styleOverride); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setShadow: function(ctx) { + if (!this.shadow) { + return; + } + + var shadow = this.shadow, canvas = this.canvas, scaling, + multX = (canvas && canvas.viewportTransform[0]) || 1, + multY = (canvas && canvas.viewportTransform[3]) || 1; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + if (canvas && canvas._isRetinaScaling()) { + multX *= fabric.devicePixelRatio; + multY *= fabric.devicePixelRatio; + } + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * + (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _removeShadow: function(ctx) { + if (!this.shadow) { + return; + } + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} filler fabric.Pattern or fabric.Gradient + * @return {Object} offset.offsetX offset for text rendering + * @return {Object} offset.offsetY offset for text rendering + */ + _applyPatternGradientTransform: function(ctx, filler) { + if (!filler || !filler.toLive) { + return { offsetX: 0, offsetY: 0 }; + } + var t = filler.gradientTransform || filler.patternTransform; + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; + + if (filler.gradientUnits === 'percentage') { + ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); + } + else { + ctx.transform(1, 0, 0, 1, offsetX, offsetY); + } + if (t) { + ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); + } + return { offsetX: offsetX, offsetY: offsetY }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderPaintInOrder: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderStroke(ctx); + this._renderFill(ctx); + } + else { + this._renderFill(ctx); + this._renderStroke(ctx); + } + }, + + /** + * @private + * function that actually render something on the context. + * empty here to allow Obects to work on tests to benchmark fabric functionalites + * not related to rendering + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(/* ctx */) { + + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderFill: function(ctx) { + if (!this.fill) { + return; + } + + ctx.save(); + this._setFillStyles(ctx, this); + if (this.fillRule === 'evenodd') { + ctx.fill('evenodd'); + } + else { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderStroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + if (this.strokeUniform && this.group) { + var scaling = this.getObjectScaling(); + ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); + } + else if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); + } + this._setLineDash(ctx, this.strokeDashArray); + this._setStrokeStyles(ctx, this); + ctx.stroke(); + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drwabacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Gradient} filler a fabric gradient instance + */ + _applyPatternForTransformedGradient: function(ctx, filler) { + var dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + pCanvas = fabric.util.createCanvasElement(), pCtx, retinaScaling = this.canvas.getRetinaScaling(), + width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.scale( + dims.zoomX / this.scaleX / retinaScaling, + dims.zoomY / this.scaleY / retinaScaling + ); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fillStyle = filler.toLive(ctx); + pCtx.fill(); + ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); + ctx.scale( + retinaScaling * this.scaleX / dims.zoomX, + retinaScaling * this.scaleY / dims.zoomY + ); + ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { x: this.left + this.width / 2, y: this.top + this.height / 2 }; + }, + + /** + * This function is an helper for svg import. it decompose the transformMatrix + * and assign properties to object. + * untransformed coordinates + * @private + * @chainable + */ + _assignTransformMatrixProps: function() { + if (this.transformMatrix) { + var options = fabric.util.qrDecompose(this.transformMatrix); + this.flipX = false; + this.flipY = false; + this.set('scaleX', options.scaleX); + this.set('scaleY', options.scaleY); + this.angle = options.angle; + this.skewX = options.skewX; + this.skewY = 0; + } + }, + + /** + * This function is an helper for svg import. it removes the transform matrix + * and set to object properties that fabricjs can handle + * @private + * @param {Object} preserveAspectRatioOptions + * @return {thisArg} + */ + _removeTransformMatrix: function(preserveAspectRatioOptions) { + var center = this._findCenterFromElement(); + if (this.transformMatrix) { + this._assignTransformMatrixProps(); + center = fabric.util.transformPoint(center, this.transformMatrix); + } + this.transformMatrix = null; + if (preserveAspectRatioOptions) { + this.scaleX *= preserveAspectRatioOptions.scaleX; + this.scaleY *= preserveAspectRatioOptions.scaleY; + this.cropX = preserveAspectRatioOptions.cropX; + this.cropY = preserveAspectRatioOptions.cropY; + center.x += preserveAspectRatioOptions.offsetLeft; + center.y += preserveAspectRatioOptions.offsetTop; + this.width = preserveAspectRatioOptions.width; + this.height = preserveAspectRatioOptions.height; + } + this.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * Clones an instance, using a callback method will work for every object. + * @param {Function} callback Callback is invoked with a clone as a first argument + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + */ + clone: function(callback, propertiesToInclude) { + var objectForm = this.toObject(propertiesToInclude); + if (this.constructor.fromObject) { + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); + } + }, + + /** + * Creates an instance of fabric.Image out of an object + * makes use of toCanvasElement. + * Once this method was based on toDataUrl and loadImage, so it also had a quality + * and format option. toCanvasElement is faster and produce no loss of quality. + * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. + * toCanvasElement and then toBlob from the obtained canvas is also a good option. + * This method is sync now, but still support the callback because we did not want to break. + * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. + * @param {Function} callback callback, invoked with an instance as a first argument + * @param {Object} [options] for clone as image, passed to toDataURL + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {fabric.Object} thisArg + */ + cloneAsImage: function(callback, options) { + var canvasEl = this.toCanvasElement(options); + if (callback) { + callback(new fabric.Image(canvasEl)); + } + return this; + }, + + /** + * Converts an object into a HTMLCanvas element + * @param {Object} options Options object + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {HTMLCanvasElement} Returns DOM element with the fabric.Object + */ + toCanvasElement: function(options) { + options || (options = { }); + + var utils = fabric.util, origParams = utils.saveObjectTransform(this), + originalGroup = this.group, + originalShadow = this.shadow, abs = Math.abs, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); + delete this.group; + if (options.withoutTransform) { + utils.resetObjectTransform(this); + } + if (options.withoutShadow) { + this.shadow = null; + } + + var el = fabric.util.createCanvasElement(), + // skip canvas zoom and calculate with setCoords now. + boundingRect = this.getBoundingRect(true, true), + shadow = this.shadow, scaling, + shadowOffset = { x: 0, y: 0 }, shadowBlur, + width, height; + + if (shadow) { + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + // consider non scaling shadow. + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + } + width = boundingRect.width + shadowOffset.x; + height = boundingRect.height + shadowOffset.y; + // if the current width/height is not an integer + // we need to make it so. + el.width = Math.ceil(width); + el.height = Math.ceil(height); + var canvas = new fabric.StaticCanvas(el, { + enableRetinaScaling: false, + renderOnAddRemove: false, + skipOffscreen: false, + }); + if (options.format === 'jpeg') { + canvas.backgroundColor = '#fff'; + } + this.setPositionByOrigin(new fabric.Point(canvas.width / 2, canvas.height / 2), 'center', 'center'); + + var originalCanvas = this.canvas; + canvas.add(this); + var canvasEl = canvas.toCanvasElement(multiplier || 1, options); + this.shadow = originalShadow; + this.set('canvas', originalCanvas); + if (originalGroup) { + this.group = originalGroup; + } + this.set(origParams).setCoords(); + // canvas.dispose will call image.dispose that will nullify the elements + // since this canvas is a simple element for the process, we remove references + // to objects in this way in order to avoid object trashing. + canvas._objects = []; + canvas.dispose(); + canvas = null; + + return canvasEl; + }, + + /** + * Converts an object into a data-url-like string + * @param {Object} options Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toDataURL: function(options) { + options || (options = { }); + return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); + }, + + /** + * Returns true if specified type is identical to the type of an instance + * @param {String} type Type to check against + * @return {Boolean} + */ + isType: function(type) { + return this.type === type; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance (is 1 unless subclassed) + */ + complexity: function() { + return 1; + }, + + /** + * Returns a JSON representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON + */ + toJSON: function(propertiesToInclude) { + // delegate, not alias + return this.toObject(propertiesToInclude); + }, + + /** + * Sets "angle" of an instance with centered rotation + * @param {Number} angle Angle value (in degrees) + * @return {fabric.Object} thisArg + * @chainable + */ + rotate: function(angle) { + var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; + + if (shouldCenterOrigin) { + this._setOriginToCenter(); + } + + this.set('angle', angle); + + if (shouldCenterOrigin) { + this._resetOrigin(); + } + + return this; + }, + + /** + * Centers object horizontally on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerH: function () { + this.canvas && this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object horizontally on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterH: function () { + this.canvas && this.canvas.viewportCenterObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas && this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterV: function () { + this.canvas && this.canvas.viewportCenterObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + this.canvas && this.canvas.centerObject(this); + return this; + }, + + /** + * Centers object on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenter: function () { + this.canvas && this.canvas.viewportCenterObject(this); + return this; + }, + + /** + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; + }, + + /** + * Sets canvas globalCompositeOperation for specific object + * custom composition operation for the particular object can be specified using globalCompositeOperation property + * @param {CanvasRenderingContext2D} ctx Rendering canvas context + */ + _setupCompositeOperation: function (ctx) { + if (this.globalCompositeOperation) { + ctx.globalCompositeOperation = this.globalCompositeOperation; + } + } + }); + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object); + + extend(fabric.Object.prototype, fabric.Observable); + + /** + * Defines the number of fraction digits to use when serializing object values. + * You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc. + * @static + * @memberOf fabric.Object + * @constant + * @type Number + */ + fabric.Object.NUM_FRACTION_DIGITS = 2; + + fabric.Object._fromObject = function(className, object, callback, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); + }); + }; + + /** + * Unique id used internally when creating SVG elements + * @static + * @memberOf fabric.Object + * @type Number + */ + fabric.Object.__uid = 0; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + originXOffset = { + left: -0.5, + center: 0, + right: 0.5 + }, + originYOffset = { + top: -0.5, + center: 0, + bottom: 0.5 + }; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Translates the coordinates from a set of origin to another (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { + var x = point.x, + y = point.y, + offsetX, offsetY, dim; + + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; + } + + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } + + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; + + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; + } + + return new fabric.Point(x, y); + }, + + /** + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToCenterPoint: function(point, originX, originY) { + var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); + if (this.angle) { + return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {fabric.Point} center The point which corresponds to center of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToOriginPoint: function(center, originX, originY) { + var p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + if (this.angle) { + return fabric.util.rotatePoint(p, center, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Returns the real center coordinates of the object + * @return {fabric.Point} + */ + getCenterPoint: function() { + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); + }, + + /** + * Returns the coordinates of the object based on center coordinates + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @return {fabric.Point} + */ + // getOriginPoint: function(center) { + // return this.translateToOriginPoint(center, this.originX, this.originY); + // }, + + /** + * Returns the coordinates of the object as if it has a different origin + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + getPointByOrigin: function(originX, originY) { + var center = this.getCenterPoint(); + return this.translateToOriginPoint(center, originX, originY); + }, + + /** + * Returns the point in local coordinates + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { + p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + } + else { + p = new fabric.Point(this.left, this.top); + } + + p2 = new fabric.Point(point.x, point.y); + if (this.angle) { + p2 = fabric.util.rotatePoint(p2, center, -degreesToRadians(this.angle)); + } + return p2.subtractEquals(p); + }, + + /** + * Returns the point in global coordinates + * @param {fabric.Point} The point relative to the local coordinate system + * @return {fabric.Point} + */ + // toGlobalPoint: function(point) { + // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); + // }, + + /** + * Sets the position of the object taking into consideration the object's origin + * @param {fabric.Point} pos The new position of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {void} + */ + setPositionByOrigin: function(pos, originX, originY) { + var center = this.translateToCenterPoint(pos, originX, originY), + position = this.translateToOriginPoint(center, this.originX, this.originY); + this.set('left', position.x); + this.set('top', position.y); + }, + + /** + * @param {String} to One of 'left', 'center', 'right' + */ + adjustPosition: function(to) { + var angle = degreesToRadians(this.angle), + hypotFull = this.getScaledWidth(), + xFull = fabric.util.cos(angle) * hypotFull, + yFull = fabric.util.sin(angle) * hypotFull, + offsetFrom, offsetTo; + + //TODO: this function does not consider mixed situation like top, center. + if (typeof this.originX === 'string') { + offsetFrom = originXOffset[this.originX]; + } + else { + offsetFrom = this.originX - 0.5; + } + if (typeof to === 'string') { + offsetTo = originXOffset[to]; + } + else { + offsetTo = to - 0.5; + } + this.left += xFull * (offsetTo - offsetFrom); + this.top += yFull * (offsetTo - offsetFrom); + this.setCoords(); + this.originX = to; + }, + + /** + * Sets the origin/position of the object to it's center point + * @private + * @return {void} + */ + _setOriginToCenter: function() { + this._originalOriginX = this.originX; + this._originalOriginY = this.originY; + + var center = this.getCenterPoint(); + + this.originX = 'center'; + this.originY = 'center'; + + this.left = center.x; + this.top = center.y; + }, + + /** + * Resets the origin/position of the object to it's original origin + * @private + * @return {void} + */ + _resetOrigin: function() { + var originPoint = this.translateToOriginPoint( + this.getCenterPoint(), + this._originalOriginX, + this._originalOriginY); + + this.originX = this._originalOriginX; + this.originY = this._originalOriginY; + + this.left = originPoint.x; + this.top = originPoint.y; + + this._originalOriginX = null; + this._originalOriginY = null; + }, + + /** + * @private + */ + _getLeftTopCoords: function() { + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + }, + }); + +})(); + + +(function() { + + function arrayFromCoords(coords) { + return [ + new fabric.Point(coords.tl.x, coords.tl.y), + new fabric.Point(coords.tr.x, coords.tr.y), + new fabric.Point(coords.br.x, coords.br.y), + new fabric.Point(coords.bl.x, coords.bl.y) + ]; + } + + var util = fabric.util, + degreesToRadians = util.degreesToRadians, + multiplyMatrices = util.multiplyTransformMatrices, + transformPoint = util.transformPoint; + + util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Describe object's corner position in canvas element coordinates. + * properties are depending on control keys and padding the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from the controls positionHandler and are used + * to draw and locate controls + * @memberOf fabric.Object.prototype + */ + oCoords: null, + + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are useful to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcACoords(); + * @memberOf fabric.Object.prototype + */ + aCoords: null, + + /** + * Describe object's corner position in canvas element coordinates. + * includes padding. Used of object detection. + * set and refreshed with setCoords and calcCoords. + * @memberOf fabric.Object.prototype + */ + lineCoords: null, + + /** + * storage for object transform matrix + */ + ownMatrixCache: null, + + /** + * storage for object full transform matrix + */ + matrixCache: null, + + /** + * custom controls interface + * controls are added by default_controls.js + */ + controls: { }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * @param {Boolean} absolute will return aCoords if true or lineCoords + * @return {Object} {tl, tr, br, bl} points + */ + _getCoords: function(absolute, calculate) { + if (calculate) { + return (absolute ? this.calcACoords() : this.calcLineCoords()); + } + if (!this.aCoords || !this.lineCoords) { + this.setCoords(true); + } + return (absolute ? this.aCoords : this.lineCoords); + }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * The coords are returned in an array. + * @return {Array} [tl, tr, br, bl] of points + */ + getCoords: function(absolute, calculate) { + return arrayFromCoords(this._getCoords(absolute, calculate)); + }, + + /** + * Checks if object intersects with an area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect: function(pointTL, pointBR, absolute, calculate) { + var coords = this.getCoords(absolute, calculate), + intersection = fabric.Intersection.intersectPolygonRectangle( + coords, + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject: function(other, absolute, calculate) { + var intersection = fabric.Intersection.intersectPolygonPolygon( + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); + + return intersection.status === 'Intersection' + || other.isContainedWithinObject(this, absolute, calculate) + || this.isContainedWithinObject(other, absolute, calculate); + }, + + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject: function(other, absolute, calculate) { + var points = this.getCoords(absolute, calculate), + otherCoords = absolute ? other.aCoords : other.lineCoords, + i = 0, lines = other._getImageLines(otherCoords); + for (; i < 4; i++) { + if (!other.containsPoint(points[i], lines)) { + return false; + } + } + return true; + }, + + /** + * Checks if object is fully contained within area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect: function(pointTL, pointBR, absolute, calculate) { + var boundingRect = this.getBoundingRect(absolute, calculate); + + return ( + boundingRect.left >= pointTL.x && + boundingRect.left + boundingRect.width <= pointBR.x && + boundingRect.top >= pointTL.y && + boundingRect.top + boundingRect.height <= pointBR.y + ); + }, + + /** + * Checks if point is inside the object + * @param {fabric.Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if point is inside the object + */ + containsPoint: function(point, lines, absolute, calculate) { + var coords = this._getCoords(absolute, calculate), + lines = lines || this._getImageLines(coords), + xPoints = this._findCrossPoints(point, lines); + // if xPoints is odd then point is inside the object + return (xPoints !== 0 && xPoints % 2 === 1); + }, + + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appears on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords + * @return {Boolean} true if object is fully or partially contained within canvas + */ + isOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + var points = this.getCoords(true, calculate); + // if some point is on screen, the object is on screen. + if (points.some(function(point) { + return point.x <= pointBR.x && point.x >= pointTL.x && + point.y <= pointBR.y && point.y >= pointTL.y; + })) { + return true; + } + // no points on screen, check intersection with absolute coordinates + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + return this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Checks if the object contains the midpoint between canvas extremities + * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen + * @private + * @param {Fabric.Point} pointTL Top Left point + * @param {Fabric.Point} pointBR Top Right point + * @param {Boolean} calculate use coordinates of current position instead of .oCoords + * @return {Boolean} true if the object contains the point + */ + _containsCenterOfCanvas: function(pointTL, pointBR, calculate) { + // worst case scenario the object is so big that contains the screen + var centerPoint = { x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2 }; + if (this.containsPoint(centerPoint, null, true, calculate)) { + return true; + } + return false; + }, + + /** + * Checks if object is partially contained within the canvas with current viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is partially contained within canvas + */ + isPartiallyOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + var allPointsAreOutside = this.getCoords(true, calculate).every(function(point) { + return (point.x >= pointBR.x || point.x <= pointTL.x) && + (point.y >= pointBR.y || point.y <= pointTL.y); + }); + return allPointsAreOutside && this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} oCoords Coordinates of the object corners + */ + _getImageLines: function(oCoords) { + + var lines = { + topline: { + o: oCoords.tl, + d: oCoords.tr + }, + rightline: { + o: oCoords.tr, + d: oCoords.br + }, + bottomline: { + o: oCoords.br, + d: oCoords.bl + }, + leftline: { + o: oCoords.bl, + d: oCoords.tl + } + }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; + }, + + /** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {fabric.Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + */ + // remove yi, not used but left code here just in case. + _findCrossPoints: function(point, lines) { + var b1, b2, a1, a2, xi, // yi, + xcount = 0, + iLine; + + for (var lineKey in lines) { + iLine = lines[lineKey]; + // optimisation 1: line below point. no cross + if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { + continue; + } + // optimisation 2: line above point. no cross + if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { + xi = iLine.o.x; + // yi = point.y; + } + // calculate the intersection point + else { + b1 = 0; + b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + a1 = point.y - b1 * point.x; + a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + // yi = a1 + b1 * xi; + } + // dont count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimisation 4: specific for square images + if (xcount === 2) { + break; + } + } + return xcount; + }, + + /** + * Returns coordinates of object's bounding rectangle (left, top, width, height) + * the box is intended as aligned to axis of canvas. + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect: function(absolute, calculate) { + var coords = this.getCoords(absolute, calculate); + return util.makeBoundingBoxFromPoints(coords); + }, + + /** + * Returns width of an object's bounding box counting transformations + * before 2.0 it was named getWidth(); + * @return {Number} width value + */ + getScaledWidth: function() { + return this._getTransformedDimensions().x; + }, + + /** + * Returns height of an object bounding box counting transformations + * before 2.0 it was named getHeight(); + * @return {Number} height value + */ + getScaledHeight: function() { + return this._getTransformedDimensions().y; + }, + + /** + * Makes sure the scale is valid and modifies it if necessary + * @private + * @param {Number} value + * @return {Number} + */ + _constrainScale: function(value) { + if (Math.abs(value) < this.minScaleLimit) { + if (value < 0) { + return -this.minScaleLimit; + } + else { + return this.minScaleLimit; + } + } + else if (value === 0) { + return 0.0001; + } + return value; + }, + + /** + * Scales an object (equally by x and y) + * @param {Number} value Scale factor + * @return {fabric.Object} thisArg + * @chainable + */ + scale: function(value) { + this._set('scaleX', value); + this._set('scaleY', value); + return this.setCoords(); + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New width value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).width / this.getScaledWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New height value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Calculates and returns the .coords of an object. + * unused by the library, only for the end dev. + * @return {Object} Object with tl, tr, br, bl .... + * @chainable + * @deprecated + */ + calcCoords: function(absolute) { + // this is a compatibility function to avoid removing calcCoords now. + if (absolute) { + return this.calcACoords(); + } + return this.calcOCoords(); + }, + + calcLineCoords: function() { + var vpt = this.getViewportTransform(), + padding = this.padding, angle = degreesToRadians(this.angle), + cos = util.cos(angle), sin = util.sin(angle), + cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, + cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); + + var lineCoords = { + tl: transformPoint(aCoords.tl, vpt), + tr: transformPoint(aCoords.tr, vpt), + bl: transformPoint(aCoords.bl, vpt), + br: transformPoint(aCoords.br, vpt), + }; + + if (padding) { + lineCoords.tl.x -= cosPMinusSinP; + lineCoords.tl.y -= cosPSinP; + lineCoords.tr.x += cosPSinP; + lineCoords.tr.y -= cosPMinusSinP; + lineCoords.bl.x -= cosPSinP; + lineCoords.bl.y += cosPMinusSinP; + lineCoords.br.x += cosPMinusSinP; + lineCoords.br.y += cosPSinP; + } + + return lineCoords; + }, + + calcOCoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + vpt = this.getViewportTransform(), + startMatrix = multiplyMatrices(vpt, translateMatrix), + finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), + finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), + dim = this._calculateCurrentDimensions(), + coords = {}; + this.forEachControl(function(control, key, fabricObject) { + coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); + }); + + // debug code + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // Object.keys(coords).forEach(function(key) { + // var control = coords[key]; + // canvas.contextTop.fillRect(control.x, control.y, 3, 3); + // }); + // }, 50); + return coords; + }, + + calcACoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + finalMatrix = multiplyMatrices(translateMatrix, rotateMatrix), + dim = this._getTransformedDimensions(), + w = dim.x / 2, h = dim.y / 2; + return { + // corners + tl: transformPoint({ x: -w, y: -h }, finalMatrix), + tr: transformPoint({ x: w, y: -h }, finalMatrix), + bl: transformPoint({ x: -w, y: h }, finalMatrix), + br: transformPoint({ x: w, y: h }, finalMatrix) + }; + }, + + /** + * Sets corner and controls position coordinates based on current angle, width and height, left and top. + * oCoords are used to find the corners + * aCoords are used to quickly find an object on the canvas + * lineCoords are used to quickly find object during pointer events. + * See {@link https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords|When-to-call-setCoords} + * @param {Boolean} [skipCorners] skip calculation of oCoords. + * @return {fabric.Object} thisArg + * @chainable + */ + setCoords: function(skipCorners) { + this.aCoords = this.calcACoords(); + // in case we are in a group, for how the inner group target check works, + // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. + this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); + if (skipCorners) { + return this; + } + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this.oCoords = this.calcOCoords(); + this._setCornerCoords && this._setCornerCoords(); + return this; + }, + + /** + * calculate rotation matrix of an object + * @return {Array} rotation matrix for the object + */ + _calcRotateMatrix: function() { + return util.calcRotateMatrix(this); + }, + + /** + * calculate the translation matrix for an object transform + * @return {Array} rotation matrix for the object + */ + _calcTranslateMatrix: function() { + var center = this.getCenterPoint(); + return [1, 0, 0, 1, center.x, center.y]; + }, + + transformMatrixKey: function(skipGroup) { + var sep = '_', prefix = ''; + if (!skipGroup && this.group) { + prefix = this.group.transformMatrixKey(skipGroup) + sep; + }; + return prefix + this.top + sep + this.left + sep + this.scaleX + sep + this.scaleY + + sep + this.skewX + sep + this.skewY + sep + this.angle + sep + this.originX + sep + this.originY + + sep + this.width + sep + this.height + sep + this.strokeWidth + this.flipX + this.flipY; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties. + * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations + * There are some situation in which this is useful to avoid the fake rotation. + * @return {Array} transform matrix for the object + */ + calcTransformMatrix: function(skipGroup) { + var matrix = this.calcOwnMatrix(); + if (skipGroup || !this.group) { + return matrix; + } + var key = this.transformMatrixKey(skipGroup), cache = this.matrixCache || (this.matrixCache = {}); + if (cache.key === key) { + return cache.value; + } + if (this.group) { + matrix = multiplyMatrices(this.group.calcTransformMatrix(false), matrix); + } + cache.key = key; + cache.value = matrix; + return matrix; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {Array} transform matrix for the object + */ + calcOwnMatrix: function() { + var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); + if (cache.key === key) { + return cache.value; + } + var tMatrix = this._calcTranslateMatrix(), + options = { + angle: this.angle, + translateX: tMatrix[4], + translateY: tMatrix[5], + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + flipX: this.flipX, + flipY: this.flipY, + }; + cache.key = key; + cache.value = util.composeMatrix(options); + return cache.value; + }, + + /* + * Calculate object dimensions from its properties + * @private + * @deprecated since 3.4.0, please use fabric.util._calcDimensionsTransformMatrix + * not including or including flipX, flipY to emulate the flipping boolean + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { + return util.calcDimensionsMatrix({ + skewX: skewX, + skewY: skewY, + scaleX: this.scaleX * (flipping && this.flipX ? -1 : 1), + scaleY: this.scaleY * (flipping && this.flipY ? -1 : 1) + }); + }, + + /* + * Calculate object dimensions from its properties + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getNonTransformedDimensions: function() { + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param {Number} skewX, a value to override current skewX + * @param {Number} skewY, a value to override current skewY + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions, dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + + if (this.strokeUniform) { + dimX = this.width; + dimY = this.height; + } + else { + dimensions = this._getNonTransformedDimensions(); + dimX = dimensions.x; + dimY = dimensions.y; + } + if (noSkew) { + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + } + var bbox = util.sizeAfterTransform(dimX, dimY, { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: skewX, + skewY: skewY, + }); + return this._finalizeDimensions(bbox.x, bbox.y); + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param Number width width of the bbox + * @param Number height height of the bbox + * @private + * @return {Object} .x finalized width dimension + * @return {Object} .y finalized height dimension + */ + _finalizeDimensions: function(width, height) { + return this.strokeUniform ? + { x: width + this.strokeWidth, y: height + this.strokeWidth } + : + { x: width, y: height }; + }, + + /* + * Calculate object dimensions for controls box, including padding and canvas zoom. + * and active selection + * private + */ + _calculateCurrentDimensions: function() { + var vpt = this.getViewportTransform(), + dim = this._getTransformedDimensions(), + p = transformPoint(dim, vpt, true); + return p.scalarAdd(2 * this.padding); + }, + }); +})(); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else if (this.canvas) { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else if (this.canvas) { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else if (this.canvas) { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else if (this.canvas) { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group && this.group.type !== 'activeSelection') { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else if (this.canvas) { + this.canvas.moveTo(this, index); + } + return this; + } +}); + + +/* _TO_SVG_START_ */ +(function() { + function getSvgColorString(prop, value) { + if (!value) { + return prop + ': none; '; + } + else if (value.toLive) { + return prop + ': url(#SVGID_' + value.id + '); '; + } + else { + var color = new fabric.Color(value), + str = prop + ': ' + color.toRgb() + '; ', + opacity = color.getAlpha(); + if (opacity !== 1) { + //change the color in rgb + opacity + str += prop + '-opacity: ' + opacity.toString() + '; '; + } + return str; + } + } + + var toFixed = fabric.util.toFixed; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + + var fillRule = this.fillRule ? this.fillRule : 'nonzero', + strokeWidth = this.strokeWidth ? this.strokeWidth : '0', + strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none', + strokeDashOffset = this.strokeDashOffset ? this.strokeDashOffset : '0', + strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', + strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', + strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', + opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', + visibility = this.visible ? '' : ' visibility: hidden;', + filter = skipShadow ? '' : this.getSvgFilter(), + fill = getSvgColorString('fill', this.fill), + stroke = getSvgColorString('stroke', this.stroke); + + return [ + stroke, + 'stroke-width: ', strokeWidth, '; ', + 'stroke-dasharray: ', strokeDashArray, '; ', + 'stroke-linecap: ', strokeLineCap, '; ', + 'stroke-dashoffset: ', strokeDashOffset, '; ', + 'stroke-linejoin: ', strokeLineJoin, '; ', + 'stroke-miterlimit: ', strokeMiterLimit, '; ', + fill, + 'fill-rule: ', fillRule, '; ', + 'opacity: ', opacity, ';', + filter, + visibility + ].join(''); + }, + + /** + * Returns styles-string for svg-export + * @param {Object} style the object from which to retrieve style properties + * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. + * @return {String} + */ + getSvgSpanStyles: function(style, useWhiteSpace) { + var term = '; '; + var fontFamily = style.fontFamily ? + 'font-family: ' + (((style.fontFamily.indexOf('\'') === -1 && style.fontFamily.indexOf('"') === -1) ? + '\'' + style.fontFamily + '\'' : style.fontFamily)) + term : ''; + var strokeWidth = style.strokeWidth ? 'stroke-width: ' + style.strokeWidth + term : '', + fontFamily = fontFamily, + fontSize = style.fontSize ? 'font-size: ' + style.fontSize + 'px' + term : '', + fontStyle = style.fontStyle ? 'font-style: ' + style.fontStyle + term : '', + fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + term : '', + fill = style.fill ? getSvgColorString('fill', style.fill) : '', + stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '', + textDecoration = this.getSvgTextDecoration(style), + deltaY = style.deltaY ? 'baseline-shift: ' + (-style.deltaY) + '; ' : ''; + if (textDecoration) { + textDecoration = 'text-decoration: ' + textDecoration + term; + } + + return [ + stroke, + strokeWidth, + fontFamily, + fontSize, + fontStyle, + fontWeight, + textDecoration, + fill, + deltaY, + useWhiteSpace ? 'white-space: pre; ' : '' + ].join(''); + }, + + /** + * Returns text-decoration property for svg-export + * @param {Object} style the object from which to retrieve style properties + * @return {String} + */ + getSvgTextDecoration: function(style) { + return ['overline', 'underline', 'line-through'].filter(function(decoration) { + return style[decoration.replace('-', '')]; + }).join(' '); + }, + + /** + * Returns filter for svg shadow + * @return {String} + */ + getSvgFilter: function() { + return this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + }, + + /** + * Returns id attribute for svg output + * @return {String} + */ + getSvgCommons: function() { + return [ + this.id ? 'id="' + this.id + '" ' : '', + this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '', + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @param {Boolean} use the full transform or the single object one. + * @return {String} + */ + getSvgTransform: function(full, additionalTransform) { + var transform = full ? this.calcTransformMatrix() : this.calcOwnMatrix(), + svgTransform = 'transform="' + fabric.util.matrixToSVG(transform); + return svgTransform + + (additionalTransform || '') + '" '; + }, + + _setSVGBg: function(textBgRects) { + if (this.backgroundColor) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + } + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + return this._createBaseSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + return '\t' + this._createBaseClipPathSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * @private + */ + _createBaseClipPathSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var reviver = options.reviver, + additionalTransform = options.additionalTransform || '', + commonPieces = [ + this.getSvgTransform(true, additionalTransform), + this.getSvgCommons(), + ].join(''), + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'); + objectMarkup[index] = commonPieces; + return reviver ? reviver(objectMarkup.join('')) : objectMarkup.join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var noStyle = options.noStyle, + reviver = options.reviver, + styleInfo = noStyle ? '' : 'style="' + this.getSvgStyles() + '" ', + shadowInfo = options.withShadow ? 'style="' + this.getSvgFilter() + '" ' : '', + clipPath = this.clipPath, + vectorEffect = this.strokeUniform ? 'vector-effect="non-scaling-stroke" ' : '', + absoluteClipPath = clipPath && clipPath.absolutePositioned, + stroke = this.stroke, fill = this.fill, shadow = this.shadow, + commonPieces, markup = [], clipPathMarkup, + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'), + additionalTransform = options.additionalTransform; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + clipPathMarkup = '\n' + + clipPath.toClipPathSVG(reviver) + + '\n'; + } + if (absoluteClipPath) { + markup.push( + '\n' + ); + } + markup.push( + '\n' + ); + commonPieces = [ + styleInfo, + vectorEffect, + noStyle ? '' : this.addPaintOrder(), ' ', + additionalTransform ? 'transform="' + additionalTransform + '" ' : '', + ].join(''); + objectMarkup[index] = commonPieces; + if (fill && fill.toLive) { + markup.push(fill.toSVG(this)); + } + if (stroke && stroke.toLive) { + markup.push(stroke.toSVG(this)); + } + if (shadow) { + markup.push(shadow.toSVG(this)); + } + if (clipPath) { + markup.push(clipPathMarkup); + } + markup.push(objectMarkup.join('')); + markup.push('\n'); + absoluteClipPath && markup.push('\n'); + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + addPaintOrder: function() { + return this.paintFirst !== 'fill' ? ' paint-order="' + this.paintFirst + '" ' : ''; + } + }); +})(); +/* _TO_SVG_END_ */ + + +(function() { + + var extend = fabric.util.object.extend, + originalSet = 'stateProperties'; + + /* + Depends on `stateProperties` + */ + function saveProps(origin, destination, props) { + var tmpObj = { }, deep = true; + props.forEach(function(prop) { + tmpObj[prop] = origin[prop]; + }); + + extend(origin[destination], tmpObj, deep); + } + + function _isEqual(origValue, currentValue, firstPass) { + if (origValue === currentValue) { + // if the objects are identical, return + return true; + } + else if (Array.isArray(origValue)) { + if (!Array.isArray(currentValue) || origValue.length !== currentValue.length) { + return false; + } + for (var i = 0, len = origValue.length; i < len; i++) { + if (!_isEqual(origValue[i], currentValue[i])) { + return false; + } + } + return true; + } + else if (origValue && typeof origValue === 'object') { + var keys = Object.keys(origValue), key; + if (!currentValue || + typeof currentValue !== 'object' || + (!firstPass && keys.length !== Object.keys(currentValue).length) + ) { + return false; + } + for (var i = 0, len = keys.length; i < len; i++) { + key = keys[i]; + // since clipPath is in the statefull cache list and the clipPath objects + // would be iterated as an object, this would lead to possible infinite recursion + // we do not want to compare those. + if (key === 'canvas' || key === 'group') { + continue; + } + if (!_isEqual(origValue[key], currentValue[key])) { + return false; + } + } + return true; + } + } + + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns true if object state (one of its state properties) was changed + * @param {String} [propertySet] optional name for the set of property we want to save + * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called + */ + hasStateChanged: function(propertySet) { + propertySet = propertySet || originalSet; + var dashedPropertySet = '_' + propertySet; + if (Object.keys(this[dashedPropertySet]).length < this[propertySet].length) { + return true; + } + return !_isEqual(this[dashedPropertySet], this, true); + }, + + /** + * Saves state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + saveState: function(options) { + var propertySet = options && options.propertySet || originalSet, + destination = '_' + propertySet; + if (!this[destination]) { + return this.setupState(options); + } + saveProps(this, destination, this[propertySet]); + if (options && options.stateProperties) { + saveProps(this, destination, options.stateProperties); + } + return this; + }, + + /** + * Setups state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + setupState: function(options) { + options = options || { }; + var propertySet = options.propertySet || originalSet; + options.propertySet = propertySet; + this['_' + propertySet] = { }; + this.saveState(options); + return this; + } + }); +})(); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Determines which corner has been clicked + * @private + * @param {Object} pointer The pointer indicating the mouse position + * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found + */ + _findTargetCorner: function(pointer, forTouch) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { + return false; + } + + var ex = pointer.x, + ey = pointer.y, + xPoints, + lines, keys = Object.keys(this.oCoords), + j = keys.length - 1, i; + this.__corner = 0; + + // cycle in reverse order so we pick first the one on top + for (; j >= 0; j--) { + i = keys[j]; + if (!this.isControlVisible(i)) { + continue; + } + + lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); + // // debugging + // + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); + if (xPoints !== 0 && xPoints % 2 === 1) { + this.__corner = i; + return i; + } + } + return false; + }, + + /** + * Calls a function for each control. The function gets called, + * with the control, the object that is calling the iterator and the control's key + * @param {Function} fn function to iterate over the controls over + */ + forEachControl: function(fn) { + for (var i in this.controls) { + fn(this.controls[i], i, this); + }; + }, + + /** + * Sets the coordinates of the draggable boxes in the corners of + * the image used to scale/rotate it. + * note: if we would switch to ROUND corner area, all of this would disappear. + * everything would resolve to a single point and a pythagorean theorem for the distance + * @private + */ + _setCornerCoords: function() { + var coords = this.oCoords; + + for (var control in coords) { + var controlObject = this.controls[control]; + coords[control].corner = controlObject.calcCornerCoords( + this.angle, this.cornerSize, coords[control].x, coords[control].y, false); + coords[control].touchCorner = controlObject.calcCornerCoords( + this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true); + } + }, + + /** + * Draws a colored layer behind the object, inside its selection borders. + * Requires public options: padding, selectionBackgroundColor + * this function is called when the context is transformed + * has checks to be skipped when the object is on a staticCanvas + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawSelectionBackground: function(ctx) { + if (!this.selectionBackgroundColor || + (this.canvas && !this.canvas.interactive) || + (this.canvas && this.canvas._activeObject !== this) + ) { + return this; + } + ctx.save(); + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + vpt = this.canvas.viewportTransform; + ctx.translate(center.x, center.y); + ctx.scale(1 / vpt[0], 1 / vpt[3]); + ctx.rotate(degreesToRadians(this.angle)); + ctx.fillStyle = this.selectionBackgroundColor; + ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBorders: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + var wh = this._calculateCurrentDimensions(), + strokeWidth = this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls, + shouldStroke = false; + + ctx.save(); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + if (hasControls) { + ctx.beginPath(); + this.forEachControl(function(control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + if (shouldStroke) { + ctx.stroke(); + } + } + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box when it is inside a group. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {object} options object representing current object parameters + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBordersInGroup: function(ctx, options, styleOverride) { + styleOverride = styleOverride || {}; + var bbox = fabric.util.sizeAfterTransform(this.width, this.height, options), + strokeWidth = this.strokeWidth, + strokeUniform = this.strokeUniform, + borderScaleFactor = this.borderScaleFactor, + width = + bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, + height = + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; + ctx.save(); + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + ctx.restore(); + return this; + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height + * Requires public options: cornerSize, padding + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawControls: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + ctx.save(); + var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; + ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); + ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; + if (!this.transparentCorners) { + ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; + } + this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); + this.setCoords(); + if (this.group) { + // fabricJS does not really support drawing controls inside groups, + // this piece of code here helps having at least the control in places. + // If an application needs to show some objects as selected because of some UI state + // can still call Object._renderControls() on any object they desire, independently of groups. + // using no padding, circular controls and hiding the rotating cursor is higly suggested, + matrix = this.group.calcTransformMatrix(); + } + this.forEachControl(function(control, key, fabricObject) { + p = fabricObject.oCoords[key]; + if (control.getVisibility(fabricObject, key)) { + if (matrix) { + p = fabric.util.transformPoint(p, matrix); + } + control.render(ctx, p.x, p.y, styleOverride, fabricObject); + } + }); + ctx.restore(); + + return this; + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlKey) { + return this.controls[controlKey] && this.controls[controlKey].getVisibility(this, controlKey); + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlKey, visible) { + if (!this._controlsVisibility) { + this._controlsVisibility = {}; + } + this._controlsVisibility[controlKey] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to deselect this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onDeselect: function() { + // implemented by sub-classes, as needed. + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to select this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onSelect: function() { + // implemented by sub-classes, as needed. + } + }); +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Animation duration (in ms) for fx* methods + * @type Number + * @default + */ + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectH: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.left, + endValue: this.getCenter().left, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('left', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Centers object vertically with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectV: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.top, + endValue: this.getCenter().top, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('top', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @param {fabric.Object} object Object to remove + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.opacity, + endValue: 0, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('opacity', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Animates object's properties + * @param {String|Object} property Property to animate (if string) or properties to animate (if object) + * @param {Number|Object} value Value to animate property to (if string was given first) or options object + * @return {fabric.Object} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation} + * @chainable + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + var propsToAnimate = [], prop, skipCallbacks; + for (prop in arguments[0]) { + propsToAnimate.push(prop); + } + for (var i = 0, len = propsToAnimate.length; i < len; i++) { + prop = propsToAnimate[i]; + skipCallbacks = i !== len - 1; + this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @param {String} property Property to animate + * @param {String} to Value to animate to + * @param {Object} [options] Options object + * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked + */ + _animate: function(property, to, options, skipCallbacks) { + var _this = this, propPair; + + to = to.toString(); + + if (!options) { + options = { }; + } + else { + options = fabric.util.object.clone(options); + } + + if (~property.indexOf('.')) { + propPair = property.split('.'); + } + + var propIsColor = + _this.colorProperties.indexOf(property) > -1 || + (propPair && _this.colorProperties.indexOf(propPair[1]) > -1); + + var currentValue = propPair + ? this.get(propPair[0])[propPair[1]] + : this.get(property); + + if (!('from' in options)) { + options.from = currentValue; + } + + if (!propIsColor) { + if (~to.indexOf('=')) { + to = currentValue + parseFloat(to.replace('=', '')); + } + else { + to = parseFloat(to); + } + } + + var _options = { + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + abort: options.abort && function(value, valueProgress, timeProgress) { + return options.abort.call(_this, value, valueProgress, timeProgress); + }, + onChange: function (value, valueProgress, timeProgress) { + if (propPair) { + _this[propPair[0]][propPair[1]] = value; + } + else { + _this.set(property, value); + } + if (skipCallbacks) { + return; + } + options.onChange && options.onChange(value, valueProgress, timeProgress); + }, + onComplete: function (value, valueProgress, timeProgress) { + if (skipCallbacks) { + return; + } + + _this.setCoords(); + options.onComplete && options.onComplete(value, valueProgress, timeProgress); + } + }; + + if (propIsColor) { + return fabric.util.animateColor(_options.startValue, _options.endValue, _options.duration, _options); + } + else { + return fabric.util.animate(_options); + } + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }; + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * Line class + * @class fabric.Line + * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'line', + + /** + * x value or first line edge + * @type Number + * @default + */ + x1: 0, + + /** + * y value or first line edge + * @type Number + * @default + */ + y1: 0, + + /** + * x value or second line edge + * @type Number + * @default + */ + x2: 0, + + /** + * y value or second line edge + * @type Number + * @default + */ + y2: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('x1', 'x2', 'y1', 'y2'), + + /** + * Constructor + * @param {Array} [points] Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} thisArg + */ + initialize: function(points, options) { + if (!points) { + points = [0, 0, 0, 0]; + } + + this.callSuper('initialize', options); + + this.set('x1', points[0]); + this.set('y1', points[1]); + this.set('x2', points[2]); + this.set('y2', points[3]); + + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Object} [options] Options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.width = Math.abs(this.x2 - this.x1); + this.height = Math.abs(this.y2 - this.y1); + + this.left = 'left' in options + ? options.left + : this._getLeftToOriginX(); + + this.top = 'top' in options + ? options.top + : this._getTopToOriginY(); + }, + + /** + * @private + * @param {String} key + * @param {*} value + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + if (typeof coordProps[key] !== 'undefined') { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. + */ + _getLeftToOriginX: makeEdgeToOriginGetter( + { // property names + origin: 'originX', + axis1: 'x1', + axis2: 'x2', + dimension: 'width' + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right' + } + ), + + /** + * @private + * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. + */ + _getTopToOriginY: makeEdgeToOriginGetter( + { // property names + origin: 'originY', + axis1: 'y1', + axis2: 'y2', + dimension: 'height' + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom' + } + ), + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + + var p = this.calcLinePoints(); + ctx.moveTo(p.x1, p.y1); + ctx.lineTo(p.x2, p.y2); + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = this.stroke || ctx.fillStyle; + this.stroke && this._renderStroke(ctx); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); + }, + + /* + * Calculate object dimensions from its properties + * @private + */ + _getNonTransformedDimensions: function() { + var dim = this.callSuper('_getNonTransformedDimensions'); + if (this.strokeLineCap === 'butt') { + if (this.width === 0) { + dim.y -= this.strokeWidth; + } + if (this.height === 0) { + dim.x -= this.strokeWidth; + } + } + return dim; + }, + + /** + * Recalculates line points given width and height + * @private + */ + calcLinePoints: function() { + var xMult = this.x1 <= this.x2 ? -1 : 1, + yMult = this.y1 <= this.y2 ? -1 : 1, + x1 = (xMult * this.width * 0.5), + y1 = (yMult * this.height * 0.5), + x2 = (xMult * this.width * -0.5), + y2 = (yMult * this.height * -0.5); + + return { + x1: x1, + x2: x2, + y1: y1, + y2: y2 + }; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var p = this.calcLinePoints(); + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) + * @static + * @memberOf fabric.Line + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @memberOf fabric.Line + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} [callback] callback function invoked after parsing + */ + fabric.Line.fromElement = function(element, callback, options) { + options = options || { }; + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), + points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + callback(new fabric.Line(points, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Line instance from an object representation + * @static + * @memberOf fabric.Line + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Line.fromObject = function(object, callback) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; + var options = clone(object, true); + options.points = [object.x1, object.y1, object.x2, object.y2]; + fabric.Object._fromObject('Line', options, _callback, 'points'); + }; + + /** + * Produces a function that calculates distance from canvas edge to Line origin. + */ + function makeEdgeToOriginGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + axis1 = propertyNames.axis1, + axis2 = propertyNames.axis2, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + switch (this.get(origin)) { + case nearest: + return Math.min(this.get(axis1), this.get(axis2)); + case center: + return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); + case farthest: + return Math.max(this.get(axis1), this.get(axis2)); + } + }; + + } + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + pi = Math.PI; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * Circle class + * @class fabric.Circle + * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'circle', + + /** + * Radius of this circle + * @type Number + * @default + */ + radius: 0, + + /** + * Start angle of the circle, moving clockwise + * deprecated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 0 + */ + startAngle: 0, + + /** + * End angle of the circle + * deprecated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 2Pi + */ + endAngle: pi * 2, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('radius', 'startAngle', 'endAngle'), + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Circle} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (key === 'radius') { + this.setRadius(value); + } + + return this; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['radius', 'startAngle', 'endAngle'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString, x = 0, y = 0, + angle = (this.endAngle - this.startAngle) % ( 2 * pi); + + if (angle === 0) { + svgString = [ + '\n' + ]; + } + else { + var startX = fabric.util.cos(this.startAngle) * this.radius, + startY = fabric.util.sin(this.startAngle) * this.radius, + endX = fabric.util.cos(this.endAngle) * this.radius, + endY = fabric.util.sin(this.endAngle) * this.radius, + largeFlag = angle > pi ? '1' : '0'; + svgString = [ + '\n' + ]; + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.arc( + 0, + 0, + this.radius, + this.startAngle, + this.endAngle, false); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @return {fabric.Circle} thisArg + */ + setRadius: function(value) { + this.radius = value; + return this.set('width', value * 2).set('height', value * 2); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @memberOf fabric.Circle + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @memberOf fabric.Circle + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @param {Object} [options] Options object + * @throws {Error} If value of `r` attribute is missing or invalid + */ + fabric.Circle.fromElement = function(element, callback) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + + if (!isValidRadius(parsedAttributes)) { + throw new Error('value of `r` attribute is required and can not be negative'); + } + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.radius; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.radius; + callback(new fabric.Circle(parsedAttributes)); + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius >= 0)); + } + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @memberOf fabric.Circle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {void} + */ + fabric.Circle.fromObject = function(object, callback) { + fabric.Object._fromObject('Circle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * Triangle class + * @class fabric.Triangle + * @extends fabric.Object + * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'triangle', + + /** + * Width is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + width: 100, + + /** + * Height is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + height: 100, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + ctx.moveTo(-widthBy2, heightBy2); + ctx.lineTo(0, -heightBy2); + ctx.lineTo(widthBy2, heightBy2); + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2, + points = [ + -widthBy2 + ' ' + heightBy2, + '0 ' + -heightBy2, + widthBy2 + ' ' + heightBy2 + ].join(','); + return [ + '' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Triangle} instance from an object representation + * @static + * @memberOf fabric.Triangle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * Ellipse class + * @class fabric.Ellipse + * @extends fabric.Object + * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'ellipse', + + /** + * Horizontal radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Ellipse} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this.set('rx', options && options.rx || 0); + this.set('ry', options && options.ry || 0); + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Ellipse} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + switch (key) { + + case 'rx': + this.rx = value; + this.set('width', value * 2); + break; + + case 'ry': + this.ry = value; + this.set('height', value * 2); + break; + + } + return this; + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRx: function() { + return this.get('rx') * this.get('scaleX'); + }, + + /** + * Returns Vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRy: function() { + return this.get('ry') * this.get('scaleY'); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.save(); + ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); + ctx.arc( + 0, + 0, + this.rx, + 0, + piBy2, + false); + ctx.restore(); + this._renderPaintInOrder(ctx); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @memberOf fabric.Ellipse + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @memberOf fabric.Ellipse + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, callback) { + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.rx; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.ry; + callback(new fabric.Ellipse(parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Ellipse} instance from an object representation + * @static + * @memberOf fabric.Ellipse + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {void} + */ + fabric.Ellipse.fromObject = function(object, callback) { + fabric.Object._fromObject('Ellipse', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Rect) { + fabric.warn('fabric.Rect is already defined'); + return; + } + + /** + * Rectangle class + * @class fabric.Rect + * @extends fabric.Object + * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { + + /** + * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('rx', 'ry'), + + /** + * Type of an object + * @type String + * @default + */ + type: 'rect', + + /** + * Horizontal border radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical border radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Initializes rx/ry attributes + * @private + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + + // 1x1 case (used in spray brush) optimization was removed because + // with caching and higher zoom level this makes more damage than help + + var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, + ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, + w = this.width, + h = this.height, + x = -this.width / 2, + y = -this.height / 2, + isRounded = rx !== 0 || ry !== 0, + /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ + k = 1 - 0.5522847498; + ctx.beginPath(); + + ctx.moveTo(x + rx, y); + + ctx.lineTo(x + w - rx, y); + isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); + + ctx.lineTo(x + w, y + h - ry); + isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); + + ctx.lineTo(x + rx, y + h); + isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); + + ctx.lineTo(x, y + ry); + isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); + + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var x = -this.width / 2, y = -this.height / 2; + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + * @memberOf fabric.Rect + * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement + */ + fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); + + /** + * Returns {@link fabric.Rect} instance from an SVG element + * @static + * @memberOf fabric.Rect + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Rect.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + options = options || { }; + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + parsedAttributes.height = parsedAttributes.height || 0; + parsedAttributes.width = parsedAttributes.width || 0; + var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect.visible = rect.visible && rect.width > 0 && rect.height > 0; + callback(rect); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Rect} instance from an object representation + * @static + * @memberOf fabric.Rect + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + */ + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * Polyline class + * @class fabric.Polyline + * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polyline', + + /** + * Points array + * @type Array + * @default + */ + points: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), + + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); + */ + initialize: function(points, options) { + options = options || {}; + this.points = points || []; + this.callSuper('initialize', options); + this._setPositionDimensions(options); + }, + + _setPositionDimensions: function(options) { + var calcDim = this._calcDimensions(options), correctLeftTop; + this.width = calcDim.width; + this.height = calcDim.height; + if (!options.fromSVG) { + correctLeftTop = this.translateToGivenOrigin( + { x: calcDim.left - this.strokeWidth / 2, y: calcDim.top - this.strokeWidth / 2 }, + 'left', + 'top', + this.originX, + this.originY + ); + } + if (typeof options.left === 'undefined') { + this.left = options.fromSVG ? calcDim.left : correctLeftTop.x; + } + if (typeof options.top === 'undefined') { + this.top = options.fromSVG ? calcDim.top : correctLeftTop.y; + } + this.pathOffset = { + x: calcDim.left + this.width / 2, + y: calcDim.top + this.height / 2 + }; + }, + + /** + * Calculate the polygon min and max point from points array, + * returning an object with left, top, width, height to measure the + * polygon size + * @return {Object} object.left X coordinate of the polygon leftmost point + * @return {Object} object.top Y coordinate of the polygon topmost point + * @return {Object} object.width distance between X coordinates of the polygon leftmost and rightmost point + * @return {Object} object.height distance between Y coordinates of the polygon topmost and bottommost point + * @private + */ + _calcDimensions: function() { + + var points = this.points, + minX = min(points, 'x') || 0, + minY = min(points, 'y') || 0, + maxX = max(points, 'x') || 0, + maxY = max(points, 'y') || 0, + width = (maxX - minX), + height = (maxY - minY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + points: this.points.concat() + }); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var points = [], diffX = this.pathOffset.x, diffY = this.pathOffset.y, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push( + toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', + toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' + ); + } + return [ + '<' + this.type + ' ', 'COMMON_PARTS', + 'points="', points.join(''), + '" />\n' + ]; + }, + /* _TO_SVG_END_ */ + + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + commonRender: function(ctx) { + var point, len = this.points.length, + x = this.pathOffset.x, + y = this.pathOffset.y; + + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return false; + } + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (var i = 0; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + return true; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + this._renderPaintInOrder(ctx); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.get('points').length; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) + * @static + * @memberOf fabric.Polyline + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @memberOf fabric.Polyline + * @param {SVGElement} element Element to parser + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polyline.fromElementGenerator = function(_class) { + return function(element, callback, options) { + if (!element) { + return callback(null); + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric[_class].ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric[_class](points, extend(parsedAttributes, options))); + }; + }; + + fabric.Polyline.fromElement = fabric.Polyline.fromElementGenerator('Polyline'); + + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @memberOf fabric.Polyline + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * Polygon class + * @class fabric.Polygon + * @extends fabric.Polyline + * @see {@link fabric.Polygon#initialize} for constructor definition + */ + fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polygon', + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + ctx.closePath(); + this._renderPaintInOrder(ctx); + }, + + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @memberOf fabric.Polygon + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns {@link fabric.Polygon} instance from an SVG element + * @static + * @memberOf fabric.Polygon + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @memberOf fabric.Polygon + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @return {void} + */ + fabric.Polygon.fromObject = function(object, callback) { + fabric.Object._fromObject('Polygon', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max, + extend = fabric.util.object.extend, + _toString = Object.prototype.toString, + toFixed = fabric.util.toFixed; + + if (fabric.Path) { + fabric.warn('fabric.Path is already defined'); + return; + } + + /** + * Path class + * @class fabric.Path + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition + */ + fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path', + + /** + * Array of path points + * @type Array + * @default + */ + path: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('path', 'fillRule'), + + stateProperties: fabric.Object.prototype.stateProperties.concat('path'), + + /** + * Constructor + * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) + * @param {Object} [options] Options object + * @return {fabric.Path} thisArg + */ + initialize: function(path, options) { + options = options || { }; + this.callSuper('initialize', options); + if (!path) { + path = []; + } + + var fromArray = _toString.call(path) === '[object Array]'; + + this.path = fabric.util.makePathSimpler( + fromArray ? path : fabric.util.parsePath(path) + ); + + if (!this.path) { + return; + } + fabric.Polyline.prototype._setPositionDimensions.call(this, options); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _renderPathCommands: function(ctx) { + var current, // current instruction + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + l = -this.pathOffset.x, + t = -this.pathOffset.y; + + ctx.beginPath(); + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'C': // bezierCurveTo, absolute + x = current[5]; + y = current[6]; + controlX = current[3]; + controlY = current[4]; + ctx.bezierCurveTo( + current[1] + l, + current[2] + t, + controlX + l, + controlY + t, + x + l, + y + t + ); + break; + + case 'Q': // quadraticCurveTo, absolute + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + current[3] + l, + current[4] + t + ); + x = current[3]; + y = current[4]; + controlX = current[1]; + controlY = current[2]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + ctx.closePath(); + break; + } + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _render: function(ctx) { + this._renderPathCommands(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns string representation of an instance + * @return {String} string representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + path: this.path.map(function(item) { return item.slice(); }), + }); + }, + + /** + * Returns dataless object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var o = this.toObject(['sourcePath'].concat(propertiesToInclude)); + if (o.sourcePath) { + delete o.path; + } + return o; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var path = fabric.util.joinPath(this.path); + return [ + '\n' + ]; + }, + + _getOffsetTransform: function() { + var digits = fabric.Object.NUM_FRACTION_DIGITS; + return ' translate(' + toFixed(-this.pathOffset.x, digits) + ', ' + + toFixed(-this.pathOffset.y, digits) + ')'; + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return '\t' + this._createBaseClipPathSVGMarkup( + this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform } + ); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return this._createBaseSVGMarkup(this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform }); + }, + /* _TO_SVG_END_ */ + + /** + * Returns number representation of an instance complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + */ + _calcDimensions: function() { + + var aX = [], + aY = [], + current, // current instruction + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + bounds; + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + bounds = []; + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'C': // bezierCurveTo, absolute + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + x = current[5]; + y = current[6]; + break; + + case 'Q': // quadraticCurveTo, absolute + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + current[1], + current[2], + current[3], + current[4] + ); + x = current[3]; + y = current[4]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + break; + } + bounds.forEach(function (point) { + aX.push(point.x); + aY.push(point.y); + }); + aX.push(x); + aY.push(y); + } + + var minX = min(aX) || 0, + minY = min(aY) || 0, + maxX = max(aX) || 0, + maxY = max(aY) || 0, + deltaX = maxX - minX, + deltaY = maxY - minY; + + return { + left: minX, + top: minY, + width: deltaX, + height: deltaY + }; + } + }); + + /** + * Creates an instance of fabric.Path from an object + * @static + * @memberOf fabric.Path + * @param {Object} object + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.sourcePath === 'string') { + var pathUrl = object.sourcePath; + fabric.loadSVGFromURL(pathUrl, function (elements) { + var path = elements[0]; + path.setOptions(object); + callback && callback(path); + }); + } + else { + fabric.Object._fromObject('Path', object, callback, 'path'); + } + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @memberOf fabric.Path + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); + + /** + * Creates an instance of fabric.Path from an SVG element + * @static + * @memberOf fabric.Path + * @param {SVGElement} element to parse + * @param {Function} callback Callback to invoke when an fabric.Path instance is created + * @param {Object} [options] Options object + * @param {Function} [callback] Options callback invoked after parsing is finished + */ + fabric.Path.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max; + + if (fabric.Group) { + return; + } + + /** + * Group class + * @class fabric.Group + * @extends fabric.Object + * @mixes fabric.Collection + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.Group#initialize} for constructor definition + */ + fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'group', + + /** + * Width of stroke + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets + * @type Boolean + * @default + */ + subTargetCheck: false, + + /** + * Groups are container, do not render anything on theyr own, ence no cache properties + * @type Array + * @default + */ + cacheProperties: [], + + /** + * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still + * available setting this boolean to true. + * @type Boolean + * @since 2.0.0 + * @default + */ + useSetOnGroup: false, + + /** + * Constructor + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. + * @return {Object} thisArg + */ + initialize: function(objects, options, isAlreadyGrouped) { + options = options || {}; + this._objects = []; + // if objects enclosed in a group have been grouped already, + // we cannot change properties of objects. + // Thus we need to set options to group without objects, + isAlreadyGrouped && this.callSuper('initialize', options); + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (!isAlreadyGrouped) { + var center = options && options.centerPoint; + // we want to set origins before calculating the bounding box. + // so that the topleft can be set with that in mind. + // if specific top and left are passed, are overwritten later + // with the callSuper('initialize', options) + if (options.originX !== undefined) { + this.originX = options.originX; + } + if (options.originY !== undefined) { + this.originY = options.originY; + } + // if coming from svg i do not want to calc bounds. + // i assume width and height are passed along options + center || this._calcBounds(); + this._updateObjectsCoords(center); + delete options.centerPoint; + this.callSuper('initialize', options); + } + else { + this._updateObjectsACoords(); + } + + this.setCoords(); + }, + + /** + * @private + */ + _updateObjectsACoords: function() { + var skipControls = true; + for (var i = this._objects.length; i--; ){ + this._objects[i].setCoords(skipControls); + } + }, + + /** + * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + */ + _updateObjectsCoords: function(center) { + var center = center || this.getCenterPoint(); + for (var i = this._objects.length; i--; ){ + this._updateObjectCoords(this._objects[i], center); + } + }, + + /** + * @private + * @param {Object} object + * @param {fabric.Point} center, current center of group. + */ + _updateObjectCoords: function(object, center) { + var objectLeft = object.left, + objectTop = object.top, + skipControls = true; + + object.set({ + left: objectLeft - center.x, + top: objectTop - center.y + }); + object.group = this; + object.setCoords(skipControls); + }, + + /** + * Returns string represenation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Adds an object to a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + var nested = !!this.group; + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + if (object) { + if (nested) { + // if this group is inside another group, we need to pre transform the object + fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); + } + this._objects.push(object); + object.group = this; + object._set('canvas', this.canvas); + } + this._calcBounds(); + this._updateObjectsCoords(); + this.dirty = true; + if (nested) { + this.group.addWithUpdate(); + } + else { + this.setCoords(); + } + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + this.setCoords(); + this.dirty = true; + return this; + }, + + /** + * @private + */ + _onObjectAdded: function(object) { + this.dirty = true; + object.group = this; + object._set('canvas', this.canvas); + }, + + /** + * @private + */ + _onObjectRemoved: function(object) { + this.dirty = true; + delete object.group; + }, + + /** + * @private + */ + _set: function(key, value) { + var i = this._objects.length; + if (this.useSetOnGroup) { + while (i--) { + this._objects[i].setOnGroup(key, value); + } + } + if (key === 'canvas') { + while (i--) { + this._objects[i]._set(key, value); + } + } + fabric.Object.prototype._set.call(this, key, value); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + var _includeDefaultValues = this.includeDefaultValues; + var objsToObject = this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .map(function (obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Returns object representation of an instance, in dataless mode. + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var objsToObject, sourcePath = this.sourcePath; + if (sourcePath) { + objsToObject = sourcePath; + } + else { + var _includeDefaultValues = this.includeDefaultValues; + objsToObject = this._objects.map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toDatalessObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + } + var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx) { + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group is already cached. + * @return {Boolean} + */ + shouldCache: function() { + var ownCache = fabric.Object.prototype.shouldCache.call(this); + if (ownCache) { + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + this.ownCaching = false; + return false; + } + } + } + return ownCache; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (fabric.Object.prototype.willDrawShadow.call(this)) { + return true; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return this.ownCaching || (this.group && this.group.isOnACache()); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].render(ctx); + } + this._drawClipPath(ctx); + }, + + /** + * Check if cache is dirty + */ + isCacheDirty: function(skipCanvas) { + if (this.callSuper('isCacheDirty', skipCanvas)) { + return true; + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].isCacheDirty(true)) { + if (this._cacheCanvas) { + // if this group has not a cache canvas there is nothing to clean + var x = this.cacheWidth / this.zoomX, y = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-x / 2, -y / 2, x, y); + } + return true; + } + } + return false; + }, + + /** + * Restores original state of each of group objects (original state is that which was before group was created). + * if the nested boolean is true, the original state will be restored just for the + * first group and not for all the group chain + * @private + * @param {Boolean} nested tell the function to restore object state up to the parent group and not more + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + var groupMatrix = this.calcOwnMatrix(); + this._objects.forEach(function(object) { + // instead of using _this = this; + fabric.util.addTransformToObject(object, groupMatrix); + delete object.group; + object.setCoords(); + }); + return this; + }, + + /** + * Realises the transform from this group onto the supplied object + * i.e. it tells you what would happen if the supplied object was in + * the group, and then the group was destroyed. It mutates the supplied + * object. + * Warning: this method is not useful anymore, it has been kept to no break the api. + * is not used in the fabricJS codebase + * this method will be reduced to using the utility. + * @private + * @deprecated + * @param {fabric.Object} object + * @param {Array} parentMatrix parent transformation + * @return {fabric.Object} transformedObject + */ + realizeTransform: function(object, parentMatrix) { + fabric.util.addTransformToObject(object, parentMatrix); + return object; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + // when group is destroyed objects needs to get a repaint to be eventually + // displayed on canvas. + this._objects.forEach(function(object) { + object.set('dirty', true); + }); + return this._restoreObjectsState(); + }, + + /** + * make a group an active selection, remove the group from canvas + * the group has to be on canvas for this to work. + * @return {fabric.ActiveSelection} thisArg + * @chainable + */ + toActiveSelection: function() { + if (!this.canvas) { + return; + } + var objects = this._objects, canvas = this.canvas; + this._objects = []; + var options = this.toObject(); + delete options.objects; + var activeSelection = new fabric.ActiveSelection([]); + activeSelection.set(options); + activeSelection.type = 'activeSelection'; + canvas.remove(this); + objects.forEach(function(object) { + object.group = activeSelection; + object.dirty = true; + canvas.add(object); + }); + activeSelection.canvas = canvas; + activeSelection._objects = objects; + canvas._activeObject = activeSelection; + activeSelection.setCoords(); + return activeSelection; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + ungroupOnCanvas: function() { + return this._restoreObjectsState(); + }, + + /** + * Sets coordinates of all objects inside group + * @return {fabric.Group} thisArg + * @chainable + */ + setObjectsCoords: function() { + var skipControls = true; + this.forEachObject(function(object) { + object.setCoords(skipControls); + }); + return this; + }, + + /** + * @private + */ + _calcBounds: function(onlyWidthHeight) { + var aX = [], + aY = [], + o, prop, coords, + props = ['tr', 'br', 'bl', 'tl'], + i = 0, iLen = this._objects.length, + j, jLen = props.length; + + for ( ; i < iLen; ++i) { + o = this._objects[i]; + coords = o.calcACoords(); + for (j = 0; j < jLen; j++) { + prop = props[j]; + aX.push(coords[prop].x); + aY.push(coords[prop].y); + } + o.aCoords = coords; + } + + this._getBounds(aX, aY, onlyWidthHeight); + }, + + /** + * @private + */ + _getBounds: function(aX, aY, onlyWidthHeight) { + var minXY = new fabric.Point(min(aX), min(aY)), + maxXY = new fabric.Point(max(aX), max(aY)), + top = minXY.y || 0, left = minXY.x || 0, + width = (maxXY.x - minXY.x) || 0, + height = (maxXY.y - minXY.y) || 0; + this.width = width; + this.height = height; + if (!onlyWidthHeight) { + // the bounding box always finds the topleft most corner. + // whatever is the group origin, we set up here the left/top position. + this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); + } + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function(reviver) { + var svgString = ['\n']; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; + }, + + /** + * Returns styles-string for svg-export, specific version for group + * @return {String} + */ + getSvgStyles: function() { + var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? + 'opacity: ' + this.opacity + ';' : '', + visibility = this.visible ? '' : ' visibility: hidden;'; + return [ + opacity, + this.getSvgFilter(), + visibility + ].join(''); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var svgString = []; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); + } + + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Group} instance from an object representation + * @static + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an group instance is created + */ + fabric.Group.fromObject = function(object, callback) { + var objects = object.objects, + options = fabric.util.object.clone(object, true); + delete options.objects; + if (typeof objects === 'string') { + // it has to be an url or something went wrong. + fabric.loadSVGFromURL(objects, function (elements) { + var group = fabric.util.groupSVGElements(elements, object, objects); + group.set(options); + callback && callback(group); + }); + return; + } + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + fabric.util.enlivenObjects([object.clipPath], function(enlivedClipPath) { + var options = fabric.util.object.clone(object, true); + options.clipPath = enlivedClipPath[0]; + delete options.objects; + callback && callback(new fabric.Group(enlivenedObjects, options, true)); + }); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.ActiveSelection) { + return; + } + + /** + * Group class + * @class fabric.ActiveSelection + * @extends fabric.Group + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.ActiveSelection#initialize} for constructor definition + */ + fabric.ActiveSelection = fabric.util.createClass(fabric.Group, /** @lends fabric.ActiveSelection.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'activeSelection', + + /** + * Constructor + * @param {Object} objects ActiveSelection objects + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(objects, options) { + options = options || {}; + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (options.originX) { + this.originX = options.originX; + } + if (options.originY) { + this.originY = options.originY; + } + this._calcBounds(); + this._updateObjectsCoords(); + fabric.Object.prototype.initialize.call(this, options); + this.setCoords(); + }, + + /** + * Change te activeSelection to a normal group, + * High level function that automatically adds it to canvas as + * active object. no events fired. + * @since 2.0.0 + * @return {fabric.Group} + */ + toGroup: function() { + var objects = this._objects.concat(); + this._objects = []; + var options = fabric.Object.prototype.toObject.call(this); + var newGroup = new fabric.Group([]); + delete options.type; + newGroup.set(options); + objects.forEach(function(object) { + object.canvas.remove(object); + object.group = newGroup; + }); + newGroup._objects = objects; + if (!this.canvas) { + return newGroup; + } + var canvas = this.canvas; + canvas.add(newGroup); + canvas._activeObject = newGroup; + newGroup.setCoords(); + return newGroup; + }, + + /** + * If returns true, deselection is cancelled. + * @since 2.0.0 + * @return {Boolean} [cancel] + */ + onDeselect: function() { + this.destroy(); + return false; + }, + + /** + * Returns string representation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return false; + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + * @param {Object} [childrenOverride] properties to override the children overrides + */ + _renderControls: function(ctx, styleOverride, childrenOverride) { + ctx.save(); + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + this.callSuper('_renderControls', ctx, styleOverride); + childrenOverride = childrenOverride || { }; + if (typeof childrenOverride.hasControls === 'undefined') { + childrenOverride.hasControls = false; + } + childrenOverride.forActiveSelection = true; + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx, childrenOverride); + } + ctx.restore(); + }, + }); + + /** + * Returns {@link fabric.ActiveSelection} instance from an object representation + * @static + * @memberOf fabric.ActiveSelection + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + */ + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + /** + * Image class + * @class fabric.Image + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} + * @see {@link fabric.Image#initialize} for constructor definition + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'image', + + /** + * Width of a stroke. + * For image quality a stroke multiple of 2 gives better results. + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * When calling {@link fabric.Image.getSrc}, return value from element src with `element.getAttribute('src')`. + * This allows for relative urls as image src. + * @since 2.7.0 + * @type Boolean + * @default + */ + srcFromAttribute: false, + + /** + * private + * contains last value of scaleX to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleX: 1, + + /** + * private + * contains last value of scaleY to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleY: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingX: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingY: 1, + + /** + * minimum scale factor under which any resizeFilter is triggered to resize the image + * 0 will disable the automatic resize. 1 will trigger automatically always. + * number bigger than 1 are not implemented yet. + * @type Number + */ + minimumScaleTrigger: 0.5, + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat('cropX', 'cropY'), + + /** + * key used to retrieve the texture representing this image + * @since 2.0.0 + * @type String + * @default + */ + cacheKey: '', + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropX: 0, + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropY: 0, + + /** + * Indicates whether this canvas will use image smoothing when painting this image. + * Also influence if the cacheCanvas for this image uses imageSmoothing + * @since 4.0.0-beta.11 + * @type Boolean + * @default + */ + imageSmoothing: true, + + /** + * Constructor + * Image can be initialized with any canvas drawable or a string. + * The string should be a url and will be loaded as an image. + * Canvas and Image element work out of the box, while videos require extra code to work. + * Please check video element events for seeking. + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element + * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. + * @return {fabric.Image} thisArg + */ + initialize: function(element, options) { + options || (options = { }); + this.filters = []; + this.cacheKey = 'texture' + fabric.Object.__uid++; + this.callSuper('initialize', options); + this._initElement(element, options); + }, + + /** + * Returns image element which this instance if based on + * @return {HTMLImageElement} Image element + */ + getElement: function() { + return this._element || {}; + }, + + /** + * Sets image element for this instance to a specified one. + * If filters defined they are applied to new image. + * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * @param {HTMLImageElement} element + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element, options) { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._element = element; + this._originalElement = element; + this._initConfig(options); + if (this.filters.length !== 0) { + this.applyFilters(); + } + // resizeFilters work on the already filtered copy. + // we need to apply resizeFilters AFTER normal filters. + // applyResizeFilters is run more often than normal filters + // and is triggered by user interactions rather than dev code + if (this.resizeFilter) { + this.applyResizeFilters(); + } + return this; + }, + + /** + * Delete a single texture if in webgl mode + */ + removeTexture: function(key) { + var backend = fabric.filterBackend; + if (backend && backend.evictCachesForKey) { + backend.evictCachesForKey(key); + } + }, + + /** + * Delete textures, reference to elements and eventually JSDOM cleanup + */ + dispose: function() { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._cacheContext = undefined; + ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + }, + + /** + * Get the crossOrigin value (of the corresponding image element) + */ + getCrossOrigin: function() { + return this._originalElement && (this._originalElement.crossOrigin || null); + }, + + /** + * Returns original size of an image + * @return {Object} Object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.naturalWidth || element.width, + height: element.naturalHeight || element.height + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + var w = this.width / 2, h = this.height / 2; + ctx.beginPath(); + ctx.moveTo(-w, -h); + ctx.lineTo(w, -h); + ctx.lineTo(w, h); + ctx.lineTo(-w, h); + ctx.lineTo(-w, -h); + ctx.closePath(); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var filters = []; + + this.filters.forEach(function(filterObj) { + if (filterObj) { + filters.push(filterObj.toObject()); + } + }); + var object = extend( + this.callSuper( + 'toObject', + ['cropX', 'cropY'].concat(propertiesToInclude) + ), { + src: this.getSrc(), + crossOrigin: this.getCrossOrigin(), + filters: filters, + }); + if (this.resizeFilter) { + object.resizeFilter = this.resizeFilter.toObject(); + } + return object; + }, + + /** + * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. + * @return {Boolean} + */ + hasCrop: function() { + return this.cropX || this.cropY || this.width < this._element.width || this.height < this._element.height; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString = [], imageMarkup = [], strokeSvg, element = this._element, + x = -this.width / 2, y = -this.height / 2, clipPath = '', imageRendering = ''; + if (!element) { + return []; + } + if (this.hasCrop()) { + var clipPathId = fabric.Object.__uid++; + svgString.push( + '\n', + '\t\n', + '\n' + ); + clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; + } + if (!this.imageSmoothing) { + imageRendering = '" image-rendering="optimizeSpeed'; + } + imageMarkup.push('\t\n'); + + if (this.stroke || this.strokeDashArray) { + var origFill = this.fill; + this.fill = null; + strokeSvg = [ + '\t\n' + ]; + this.fill = origFill; + } + if (this.paintFirst !== 'fill') { + svgString = svgString.concat(strokeSvg, imageMarkup); + } + else { + svgString = svgString.concat(imageMarkup, strokeSvg); + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * Returns source of an image + * @param {Boolean} filtered indicates if the src is needed for svg + * @return {String} Source of an image + */ + getSrc: function(filtered) { + var element = filtered ? this._element : this._originalElement; + if (element) { + if (element.toDataURL) { + return element.toDataURL(); + } + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } + else { + return element.src; + } + } + else { + return this.src || ''; + } + }, + + /** + * Sets source of an image + * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) + * @param {Object} [options] Options object + * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @return {fabric.Image} thisArg + * @chainable + */ + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img, isError) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this, isError); + }, this, options && options.crossOrigin); + return this; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + applyResizeFilters: function() { + var filter = this.resizeFilter, + minimumScale = this.minimumScaleTrigger, + objectScale = this.getTotalObjectScaling(), + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, + elementToFilter = this._filteredEl || this._originalElement; + if (this.group) { + this.set('dirty', true); + } + if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { + this._element = elementToFilter; + this._filterScalingX = 1; + this._filterScalingY = 1; + this._lastScaleX = scaleX; + this._lastScaleY = scaleY; + return; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + var canvasEl = fabric.util.createCanvasElement(), + cacheKey = this._filteredEl ? (this.cacheKey + '_filtered') : this.cacheKey, + sourceWidth = elementToFilter.width, sourceHeight = elementToFilter.height; + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._lastScaleX = filter.scaleX = scaleX; + this._lastScaleY = filter.scaleY = scaleY; + fabric.filterBackend.applyFilters( + [filter], elementToFilter, sourceWidth, sourceHeight, this._element, cacheKey); + this._filterScalingX = canvasEl.width / this._originalElement.width; + this._filterScalingY = canvasEl.height / this._originalElement.height; + }, + + /** + * Applies filters assigned to this image (from "filters" array) or from filter param + * @method applyFilters + * @param {Array} filters to be applied + * @param {Boolean} forResizing specify if the filter operation is a resize operation + * @return {thisArg} return the fabric.Image object + * @chainable + */ + applyFilters: function(filters) { + + filters = filters || this.filters || []; + filters = filters.filter(function(filter) { return filter && !filter.isNeutralState(); }); + this.set('dirty', true); + + // needs to clear out or WEBGL will not resize correctly + this.removeTexture(this.cacheKey + '_filtered'); + + if (filters.length === 0) { + this._element = this._originalElement; + this._filteredEl = null; + this._filterScalingX = 1; + this._filterScalingY = 1; + return this; + } + + var imgElement = this._originalElement, + sourceWidth = imgElement.naturalWidth || imgElement.width, + sourceHeight = imgElement.naturalHeight || imgElement.height; + + if (this._element === this._originalElement) { + // if the element is the same we need to create a new element + var canvasEl = fabric.util.createCanvasElement(); + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._filteredEl = canvasEl; + } + else { + // clear the existing element to get new filter data + // also dereference the eventual resized _element + this._element = this._filteredEl; + this._filteredEl.getContext('2d').clearRect(0, 0, sourceWidth, sourceHeight); + // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y + this._lastScaleX = 1; + this._lastScaleY = 1; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + fabric.filterBackend.applyFilters( + filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey); + if (this._originalElement.width !== this._element.width || + this._originalElement.height !== this._element.height) { + this._filterScalingX = this._element.width / this._originalElement.width; + this._filterScalingY = this._element.height / this._originalElement.height; + } + return this; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { + this.applyResizeFilters(); + } + this._stroke(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * it will set the imageSmoothing for the draw operation + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + fabric.Object.prototype.drawCacheOnCanvas.call(this, ctx); + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * This is the special image version where we would like to avoid caching where possible. + * Essentially images do not benefit from caching. They may require caching, and in that + * case we do it. Also caching an image usually ends in a loss of details. + * A full performance audit should be done. + * @return {Boolean} + */ + shouldCache: function() { + return this.needsItsOwnCache(); + }, + + _renderFill: function(ctx) { + var elementToDraw = this._element; + if (!elementToDraw) { + return; + } + var scaleX = this._filterScalingX, scaleY = this._filterScalingY, + w = this.width, h = this.height, min = Math.min, max = Math.max, + // crop values cannot be lesser than 0. + cropX = max(this.cropX, 0), cropY = max(this.cropY, 0), + elWidth = elementToDraw.naturalWidth || elementToDraw.width, + elHeight = elementToDraw.naturalHeight || elementToDraw.height, + sX = cropX * scaleX, + sY = cropY * scaleY, + // the width height cannot exceed element width/height, starting from the crop offset. + sW = min(w * scaleX, elWidth - sX), + sH = min(h * scaleY, elHeight - sY), + x = -w / 2, y = -h / 2, + maxDestW = min(w, elWidth / scaleX - cropX), + maxDestH = min(h, elHeight / scaleY - cropY); + + elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH); + }, + + /** + * needed to check if image needs resize + * @private + */ + _needsResize: function() { + var scale = this.getTotalObjectScaling(); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); + }, + + /** + * @private + */ + _resetWidthHeight: function() { + this.set(this.getOriginalSize()); + }, + + /** + * The Image class's initialization method. This method is automatically + * called by the constructor. + * @private + * @param {HTMLImageElement|String} element The element representing the image + * @param {Object} [options] Options object + */ + _initElement: function(element, options) { + this.setElement(fabric.util.getById(element), options); + fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initConfig: function(options) { + options || (options = { }); + this.setOptions(options); + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + + /** + * @private + * Set the width and the height of the image object, using the element or the + * options. + * @param {Object} [options] Object with width/height properties + */ + _setWidthHeight: function(options) { + options || (options = { }); + var el = this.getElement(); + this.width = options.width || el.naturalWidth || el.width || 0; + this.height = options.height || el.naturalHeight || el.height || 0; + }, + + /** + * Calculate offset for center and scale factor for the image in order to respect + * the preserveAspectRatio attribute + * @private + * @return {Object} + */ + parsePreserveAspectRatioAttribute: function() { + var pAR = fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio || ''), + rWidth = this._element.width, rHeight = this._element.height, + scaleX = 1, scaleY = 1, offsetLeft = 0, offsetTop = 0, cropX = 0, cropY = 0, + offset, pWidth = this.width, pHeight = this.height, parsedAttributes = { width: pWidth, height: pHeight }; + if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { + if (pAR.meetOrSlice === 'meet') { + scaleX = scaleY = fabric.util.findScaleToFit(this._element, parsedAttributes); + offset = (pWidth - rWidth * scaleX) / 2; + if (pAR.alignX === 'Min') { + offsetLeft = -offset; + } + if (pAR.alignX === 'Max') { + offsetLeft = offset; + } + offset = (pHeight - rHeight * scaleY) / 2; + if (pAR.alignY === 'Min') { + offsetTop = -offset; + } + if (pAR.alignY === 'Max') { + offsetTop = offset; + } + } + if (pAR.meetOrSlice === 'slice') { + scaleX = scaleY = fabric.util.findScaleToCover(this._element, parsedAttributes); + offset = rWidth - pWidth / scaleX; + if (pAR.alignX === 'Mid') { + cropX = offset / 2; + } + if (pAR.alignX === 'Max') { + cropX = offset; + } + offset = rHeight - pHeight / scaleY; + if (pAR.alignY === 'Mid') { + cropY = offset / 2; + } + if (pAR.alignY === 'Max') { + cropY = offset; + } + rWidth = pWidth / scaleX; + rHeight = pHeight / scaleY; + } + } + else { + scaleX = pWidth / rWidth; + scaleY = pHeight / rHeight; + } + return { + width: rWidth, + height: rHeight, + scaleX: scaleX, + scaleY: scaleY, + offsetLeft: offsetLeft, + offsetTop: offsetTop, + cropX: cropX, + cropY: cropY + }; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + * @default + */ + fabric.Image.CSS_CANVAS = 'canvas-img'; + + /** + * Alias for getSrc + * @static + */ + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} callback Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, isError) { + if (isError) { + callback && callback(null, true); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var image = new fabric.Image(img, object); + callback(image, false); + }); + }); + }); + }, null, object.crossOrigin); + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + fabric.util.loadImage(url, function(img, isError) { + callback && callback(new fabric.Image(img, imgOptions), isError); + }, null, imgOptions && imgOptions.crossOrigin); + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} + */ + fabric.Image.ATTRIBUTE_NAMES = + fabric.SHARED_ATTRIBUTES.concat( + 'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split(' ') + ); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} callback Callback to execute when fabric.Image object is created + * @return {fabric.Image} Instance of fabric.Image + */ + fabric.Image.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * @private + * @return {Number} angle value + */ + _getAngleValueForStraighten: function() { + var angle = this.angle % 360; + if (angle > 0) { + return Math.round((angle - 1) / 90) * 90; + } + return Math.round(angle / 90) * 90; + }, + + /** + * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) + * @return {fabric.Object} thisArg + * @chainable + */ + straighten: function() { + this.rotate(this._getAngleValueForStraighten()); + return this; + }, + + /** + * Same as {@link fabric.Object.prototype.straighten} but with animation + * @param {Object} callbacks Object with callback functions + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Object} thisArg + * @chainable + */ + fxStraighten: function(callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: this.get('angle'), + endValue: this._getAngleValueForStraighten(), + duration: this.FX_DURATION, + onChange: function(value) { + _this.rotate(value); + onChange(); + }, + onComplete: function() { + _this.setCoords(); + onComplete(); + }, + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Straightens object, then rerenders canvas + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.requestRenderAll(); + return this; + }, + + /** + * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.requestRenderAllBound + }); + return this; + } +}); + + +(function() { + + 'use strict'; + + /** + * Tests if webgl supports certain precision + * @param {WebGL} Canvas WebGL context to test on + * @param {String} Precision to test can be any of following: 'lowp', 'mediump', 'highp' + * @returns {Boolean} Whether the user's browser WebGL supports given precision. + */ + function testPrecision(gl, precision){ + var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}'; + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + return false; + } + return true; + } + + /** + * Indicate whether this filtering backend is supported by the user's browser. + * @param {Number} tileSize check if the tileSize is supported + * @returns {Boolean} Whether the user's browser supports WebGL. + */ + fabric.isWebglSupported = function(tileSize) { + if (fabric.isLikelyNode) { + return false; + } + tileSize = tileSize || fabric.WebglFilterBackend.prototype.tileSize; + var canvas = document.createElement('canvas'); + var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + var isSupported = false; + // eslint-disable-next-line + if (gl) { + fabric.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + isSupported = fabric.maxTextureSize >= tileSize; + var precisions = ['highp', 'mediump', 'lowp']; + for (var i = 0; i < 3; i++){ + if (testPrecision(gl, precisions[i])){ + fabric.webGlPrecision = precisions[i]; + break; + }; + } + } + this.isSupported = isSupported; + return isSupported; + }; + + fabric.WebglFilterBackend = WebglFilterBackend; + + /** + * WebGL filter backend. + */ + function WebglFilterBackend(options) { + if (options && options.tileSize) { + this.tileSize = options.tileSize; + } + this.setupGLContext(this.tileSize, this.tileSize); + this.captureGPUInfo(); + }; + + WebglFilterBackend.prototype = /** @lends fabric.WebglFilterBackend.prototype */ { + + tileSize: 2048, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Setup a WebGL context suitable for filtering, and bind any needed event handlers. + */ + setupGLContext: function(width, height) { + this.dispose(); + this.createWebGLCanvas(width, height); + // eslint-disable-next-line + this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); + this.chooseFastestCopyGLTo2DMethod(width, height); + }, + + /** + * Pick a method to copy data from GL context to 2d canvas. In some browsers using + * putImageData is faster than drawImage for that specific operation. + */ + chooseFastestCopyGLTo2DMethod: function(width, height) { + var canMeasurePerf = typeof window.performance !== 'undefined', canUseImageData; + try { + new ImageData(1, 1); + canUseImageData = true; + } + catch (e) { + canUseImageData = false; + } + // eslint-disable-next-line no-undef + var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; + // eslint-disable-next-line no-undef + var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; + + if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) { + return; + } + + var targetCanvas = fabric.util.createCanvasElement(); + // eslint-disable-next-line no-undef + var imageBuffer = new ArrayBuffer(width * height * 4); + if (fabric.forceGLPutImageData) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + return; + } + var testContext = { + imageBuffer: imageBuffer, + destinationWidth: width, + destinationHeight: height, + targetCanvas: targetCanvas + }; + var startTime, drawImageTime, putImageDataTime; + targetCanvas.width = width; + targetCanvas.height = height; + + startTime = window.performance.now(); + copyGLTo2DDrawImage.call(testContext, this.gl, testContext); + drawImageTime = window.performance.now() - startTime; + + startTime = window.performance.now(); + copyGLTo2DPutImageData.call(testContext, this.gl, testContext); + putImageDataTime = window.performance.now() - startTime; + + if (drawImageTime > putImageDataTime) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + } + else { + this.copyGLTo2D = copyGLTo2DDrawImage; + } + }, + + /** + * Create a canvas element and associated WebGL context and attaches them as + * class properties to the GLFilterBackend class. + */ + createWebGLCanvas: function(width, height) { + var canvas = fabric.util.createCanvasElement(); + canvas.width = width; + canvas.height = height; + var glOptions = { + alpha: true, + premultipliedAlpha: false, + depth: false, + stencil: false, + antialias: false + }, + gl = canvas.getContext('webgl', glOptions); + if (!gl) { + gl = canvas.getContext('experimental-webgl', glOptions); + } + if (!gl) { + return; + } + gl.clearColor(0, 0, 0, 0); + // this canvas can fire webglcontextlost and webglcontextrestored + this.canvas = canvas; + this.gl = gl; + }, + + /** + * Attempts to apply the requested filters to the source provided, drawing the filtered output + * to the provided target canvas. + * + * @param {Array} filters The filters to apply. + * @param {HTMLImageElement|HTMLCanvasElement} source The source to be filtered. + * @param {Number} width The width of the source input. + * @param {Number} height The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + * @param {String|undefined} cacheKey A key used to cache resources related to the source. If + * omitted, caching will be skipped. + */ + applyFilters: function(filters, source, width, height, targetCanvas, cacheKey) { + var gl = this.gl; + var cachedTexture; + if (cacheKey) { + cachedTexture = this.getCachedTexture(cacheKey, source); + } + var pipelineState = { + originalWidth: source.width || source.originalWidth, + originalHeight: source.height || source.originalHeight, + sourceWidth: width, + sourceHeight: height, + destinationWidth: width, + destinationHeight: height, + context: gl, + sourceTexture: this.createTexture(gl, width, height, !cachedTexture && source), + targetTexture: this.createTexture(gl, width, height), + originalTexture: cachedTexture || + this.createTexture(gl, width, height, !cachedTexture && source), + passes: filters.length, + webgl: true, + aPosition: this.aPosition, + programCache: this.programCache, + pass: 0, + filterBackend: this, + targetCanvas: targetCanvas + }; + var tempFbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); + filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); }); + resizeCanvasIfNeeded(pipelineState); + this.copyGLTo2D(gl, pipelineState); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.deleteTexture(pipelineState.sourceTexture); + gl.deleteTexture(pipelineState.targetTexture); + gl.deleteFramebuffer(tempFbo); + targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); + return pipelineState; + }, + + /** + * Detach event listeners, remove references, and clean up caches. + */ + dispose: function() { + if (this.canvas) { + this.canvas = null; + this.gl = null; + } + this.clearWebGLCaches(); + }, + + /** + * Wipe out WebGL-related caches. + */ + clearWebGLCaches: function() { + this.programCache = {}; + this.textureCache = {}; + }, + + /** + * Create a WebGL texture object. + * + * Accepts specific dimensions to initialize the texture to or a source image. + * + * @param {WebGLRenderingContext} gl The GL context to use for creating the texture. + * @param {Number} width The width to initialize the texture at. + * @param {Number} height The height to initialize the texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. + * @returns {WebGLTexture} + */ + createTexture: function(gl, width, height, textureImageSource) { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (textureImageSource) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImageSource); + } + else { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + return texture; + }, + + /** + * Can be optionally used to get a texture from the cache array + * + * If an existing texture is not found, a new texture is created and cached. + * + * @param {String} uniqueId A cache key to use to find an existing texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source to use to create the + * texture cache entry if one does not already exist. + */ + getCachedTexture: function(uniqueId, textureImageSource) { + if (this.textureCache[uniqueId]) { + return this.textureCache[uniqueId]; + } + else { + var texture = this.createTexture( + this.gl, textureImageSource.width, textureImageSource.height, textureImageSource); + this.textureCache[uniqueId] = texture; + return texture; + } + }, + + /** + * Clear out cached resources related to a source image that has been + * filtered previously. + * + * @param {String} cacheKey The cache key provided when the source image was filtered. + */ + evictCachesForKey: function(cacheKey) { + if (this.textureCache[cacheKey]) { + this.gl.deleteTexture(this.textureCache[cacheKey]); + delete this.textureCache[cacheKey]; + } + }, + + copyGLTo2D: copyGLTo2DDrawImage, + + /** + * Attempt to extract GPU information strings from a WebGL context. + * + * Useful information when debugging or blacklisting specific GPUs. + * + * @returns {Object} A GPU info object with renderer and vendor strings. + */ + captureGPUInfo: function() { + if (this.gpuInfo) { + return this.gpuInfo; + } + var gl = this.gl, gpuInfo = { renderer: '', vendor: '' }; + if (!gl) { + return gpuInfo; + } + var ext = gl.getExtension('WEBGL_debug_renderer_info'); + if (ext) { + var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); + var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); + if (renderer) { + gpuInfo.renderer = renderer.toLowerCase(); + } + if (vendor) { + gpuInfo.vendor = vendor.toLowerCase(); + } + } + this.gpuInfo = gpuInfo; + return gpuInfo; + }, + }; +})(); + +function resizeCanvasIfNeeded(pipelineState) { + var targetCanvas = pipelineState.targetCanvas, + width = targetCanvas.width, height = targetCanvas.height, + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight; + + if (width !== dWidth || height !== dHeight) { + targetCanvas.width = dWidth; + targetCanvas.height = dHeight; + } +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas. + * + * The WebGL canvas is assumed to be upside down, with the top-left pixel of the + * desired output image appearing in the bottom-left corner of the WebGL canvas. + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DDrawImage(gl, pipelineState) { + var glCanvas = gl.canvas, targetCanvas = pipelineState.targetCanvas, + ctx = targetCanvas.getContext('2d'); + ctx.translate(0, targetCanvas.height); // move it down again + ctx.scale(1, -1); // vertical flip + // where is my image on the big glcanvas? + var sourceY = glCanvas.height - targetCanvas.height; + ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, + targetCanvas.width, targetCanvas.height); +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData + * API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra). + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DPutImageData(gl, pipelineState) { + var targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext('2d'), + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight, + numBytes = dWidth * dHeight * 4; + + // eslint-disable-next-line no-undef + var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); + // eslint-disable-next-line no-undef + var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); + + gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8); + var imgData = new ImageData(u8Clamped, dWidth, dHeight); + ctx.putImageData(imgData, 0, 0); +} + + +(function() { + + 'use strict'; + + var noop = function() {}; + + fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; + + /** + * Canvas 2D filter backend. + */ + function Canvas2dFilterBackend() {}; + + Canvas2dFilterBackend.prototype = /** @lends fabric.Canvas2dFilterBackend.prototype */ { + evictCachesForKey: noop, + dispose: noop, + clearWebGLCaches: noop, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Apply a set of filters against a source image and draw the filtered output + * to the provided destination canvas. + * + * @param {EnhancedFilter} filters The filter to apply. + * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. + * @param {Number} sourceWidth The width of the source input. + * @param {Number} sourceHeight The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + */ + applyFilters: function(filters, sourceElement, sourceWidth, sourceHeight, targetCanvas) { + var ctx = targetCanvas.getContext('2d'); + ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); + var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var pipelineState = { + sourceWidth: sourceWidth, + sourceHeight: sourceHeight, + imageData: imageData, + originalEl: sourceElement, + originalImageData: originalImageData, + canvasEl: targetCanvas, + ctx: ctx, + filterBackend: this, + }; + filters.forEach(function(filter) { filter.applyTo(pipelineState); }); + if (pipelineState.imageData.width !== sourceWidth || pipelineState.imageData.height !== sourceHeight) { + targetCanvas.width = pipelineState.imageData.width; + targetCanvas.height = pipelineState.imageData.height; + } + ctx.putImageData(pipelineState.imageData, 0, 0); + return pipelineState; + }, + + }; +})(); + + +/** + * @namespace fabric.Image.filters + * @memberOf fabric.Image + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + */ +fabric.Image = fabric.Image || { }; +fabric.Image.filters = fabric.Image.filters || { }; + +/** + * Root filter class from which all filter classes inherit from + * @class fabric.Image.filters.BaseFilter + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'BaseFilter', + + /** + * Array of attributes to send with buffers. do not modify + * @private + */ + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + fragmentSource: 'precision highp float;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform sampler2D uTexture;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + '}', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Sets filter's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Compile this filter's shader program. + * + * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. + * @param {String} fragmentSource fragmentShader source for compilation + * @param {String} vertexSource vertexShader source for compilation + */ + createProgram: function(gl, fragmentSource, vertexSource) { + fragmentSource = fragmentSource || this.fragmentSource; + vertexSource = vertexSource || this.vertexSource; + if (fabric.webGlPrecision !== 'highp'){ + fragmentSource = fragmentSource.replace( + /precision highp float/g, + 'precision ' + fabric.webGlPrecision + ' float' + ); + } + var vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Vertex shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(vertexShader) + ); + } + + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Fragment shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(fragmentShader) + ); + } + + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Shader link error for "${this.type}" ' + + gl.getProgramInfoLog(program) + ); + } + + var attributeLocations = this.getAttributeLocations(gl, program); + var uniformLocations = this.getUniformLocations(gl, program) || { }; + uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); + uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); + return { + program: program, + attributeLocations: attributeLocations, + uniformLocations: uniformLocations + }; + }, + + /** + * Return a map of attribute names to WebGLAttributeLocation objects. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. + * @returns {Object} A map of attribute names to attribute locations. + */ + getAttributeLocations: function(gl, program) { + return { + aPosition: gl.getAttribLocation(program, 'aPosition'), + }; + }, + + /** + * Return a map of uniform names to WebGLUniformLocation objects. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. + * @returns {Object} A map of uniform names to uniform locations. + */ + getUniformLocations: function (/* gl, program */) { + // in case i do not need any special uniform i need to return an empty object + return { }; + }, + + /** + * Send attribute data from this filter to its shader program on the GPU. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} attributeLocations A map of shader attribute names to their locations. + */ + sendAttributeData: function(gl, attributeLocations, aPositionData) { + var attributeLocation = attributeLocations.aPosition; + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(attributeLocation); + gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); + }, + + _setupFrameBuffer: function(options) { + var gl = options.context, width, height; + if (options.passes > 1) { + width = options.destinationWidth; + height = options.destinationHeight; + if (options.sourceWidth !== width || options.sourceHeight !== height) { + gl.deleteTexture(options.targetTexture); + options.targetTexture = options.filterBackend.createTexture(gl, width, height); + } + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, + options.targetTexture, 0); + } + else { + // draw last filter on canvas and not to framebuffer. + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.finish(); + } + }, + + _swapTextures: function(options) { + options.passes--; + options.pass++; + var temp = options.targetTexture; + options.targetTexture = options.sourceTexture; + options.sourceTexture = temp; + }, + + /** + * Generic isNeutral implementation for one parameter based filters. + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) + * @param {Object} options + **/ + isNeutralState: function(/* options */) { + var main = this.mainParameter, + _class = fabric.Image.filters[this.type].prototype; + if (main) { + if (Array.isArray(_class[main])) { + for (var i = _class[main].length; i--;) { + if (this[main][i] !== _class[main][i]) { + return false; + } + } + return true; + } + else { + return _class[main] === this[main]; + } + } + else { + return false; + } + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + if (!options.programCache.hasOwnProperty(this.type)) { + options.programCache[this.type] = this.createProgram(options.context); + } + return options.programCache[this.type]; + }, + + /** + * Apply this filter using webgl. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.originalTexture The texture of the original input image. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyToWebGL: function(options) { + var gl = options.context; + var shader = this.retrieveShader(options); + if (options.pass === 0 && options.originalTexture) { + gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); + } + else { + gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); + } + gl.useProgram(shader.program); + this.sendAttributeData(gl, shader.attributeLocations, options.aPosition); + + gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); + gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); + + this.sendUniformData(gl, shader.uniformLocations); + gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + + bindAdditionalTexture: function(gl, texture, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, texture); + // reset active texture to 0 as usual + gl.activeTexture(gl.TEXTURE0); + }, + + unbindAdditionalTexture: function(gl, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.activeTexture(gl.TEXTURE0); + }, + + getMainParameter: function() { + return this[this.mainParameter]; + }, + + setMainParameter: function(value) { + this[this.mainParameter] = value; + }, + + /** + * Send uniform data from this filter to its shader program on the GPU. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} uniformLocations A map of shader uniform names to their locations. + */ + sendUniformData: function(/* gl, uniformLocations */) { + // Intentionally left blank. Override me in subclasses. + }, + + /** + * If needed by a 2d filter, this functions can create an helper canvas to be used + * remember that options.targetCanvas is available for use till end of chain. + */ + createHelpLayer: function(options) { + if (!options.helpLayer) { + var helpLayer = document.createElement('canvas'); + helpLayer.width = options.sourceWidth; + helpLayer.height = options.sourceHeight; + options.helpLayer = helpLayer; + } + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + var object = { type: this.type }, mainP = this.mainParameter; + if (mainP) { + object[mainP] = this[mainP]; + } + return object; + }, + + /** + * Returns a JSON representation of an instance + * @return {Object} JSON + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + } +}); + +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; +}; + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Matrix filter class + * @class fabric.Image.filters.ColorMatrix + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php} + * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl} + * @example Kodachrome filter + * var filter = new fabric.Image.filters.ColorMatrix({ + * matrix: [ + 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502, + -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203, + -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946, + 0, 0, 0, 1, 0 + ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'ColorMatrix', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform mat4 uColorMatrix;\n' + + 'uniform vec4 uConstants;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color *= uColorMatrix;\n' + + 'color += uConstants;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Colormatrix for pixels. + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ], + + mainParameter: 'matrix', + + /** + * Lock the colormatrix on the color part, skipping alpha, manly for non webgl scenario + * to save some calculation + */ + colorsOnly: true, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.matrix = this.matrix.slice(0); + }, + + /** + * Apply the ColorMatrix operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = data.length, + m = this.matrix, + r, g, b, a, i, colorsOnly = this.colorsOnly; + + for (i = 0; i < iLen; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + if (colorsOnly) { + data[i] = r * m[0] + g * m[1] + b * m[2] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + m[14] * 255; + } + else { + a = data[i + 3]; + data[i] = r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255; + data[i + 3] = r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'), + uConstants: gl.getUniformLocation(program, 'uConstants'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var m = this.matrix, + matrix = [ + m[0], m[1], m[2], m[3], + m[5], m[6], m[7], m[8], + m[10], m[11], m[12], m[13], + m[15], m[16], m[17], m[18] + ], + constants = [m[4], m[9], m[14], m[19]]; + gl.uniformMatrix4fv(uniformLocations.uColorMatrix, false, matrix); + gl.uniform4fv(uniformLocations.uConstants, constants); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + */ + fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Brightness filter class + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 0.05 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Brightness', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBrightness;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += uBrightness;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Brightness value, from -1 to 1. + * translated to -255 to 255 for 2d + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Number} brightness + * @default + */ + brightness: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'brightness', + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.brightness === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + brightness = Math.round(this.brightness * 255); + for (i = 0; i < len; i += 4) { + data[i] = data[i] + brightness; + data[i + 1] = data[i + 1] + brightness; + data[i + 2] = data[i + 2] + brightness; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBrightness: gl.getUniformLocation(program, 'uBrightness'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBrightness, this.brightness); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + */ + fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Adapted from html5rocks article + * @class fabric.Image.filters.Convolute + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Convolute', + + /* + * Opaque value (true/false) + */ + opaque: false, + + /* + * matrix for the filter, max 9x9 + */ + matrix: [0, 0, 0, 0, 1, 0, 0, 0, 0], + + /** + * Fragment source for the brightness program + */ + fragmentSource: { + Convolute_3_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_3_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_5_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_5_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_7_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_7_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_9_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_9_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + }, + + /** + * Constructor + * @memberOf fabric.Image.filters.Convolute.prototype + * @param {Object} [options] Options object + * @param {Boolean} [options.opaque=false] Opaque value (true/false) + * @param {Array} [options.matrix] Filter matrix + */ + + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var size = Math.sqrt(this.matrix.length); + var cacheKey = this.type + '_' + size + '_' + (this.opaque ? 1 : 0); + var shaderSource = this.fragmentSource[cacheKey]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + weights = this.matrix, + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side / 2), + sw = imageData.width, + sh = imageData.height, + output = options.ctx.createImageData(sw, sh), + dst = output.data, + // go through the destination image pixels + alphaFac = this.opaque ? 1 : 0, + r, g, b, a, dstOff, + scx, scy, srcOff, wt, + x, y, cx, cy; + + for (y = 0; y < sh; y++) { + for (x = 0; x < sw; x++) { + dstOff = (y * sw + x) * 4; + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + r = 0; g = 0; b = 0; a = 0; + + for (cy = 0; cy < side; cy++) { + for (cx = 0; cx < side; cx++) { + scy = y + cy - halfSide; + scx = x + cx - halfSide; + + // eslint-disable-next-line max-depth + if (scy < 0 || scy >= sh || scx < 0 || scx >= sw) { + continue; + } + + srcOff = (scy * sw + scx) * 4; + wt = weights[cy * side + cx]; + + r += data[srcOff] * wt; + g += data[srcOff + 1] * wt; + b += data[srcOff + 2] * wt; + // eslint-disable-next-line max-depth + if (!alphaFac) { + a += data[srcOff + 3] * wt; + } + } + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + if (!alphaFac) { + dst[dstOff + 3] = a; + } + else { + dst[dstOff + 3] = data[dstOff + 3]; + } + } + } + options.imageData = output; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMatrix: gl.getUniformLocation(program, 'uMatrix'), + uOpaque: gl.getUniformLocation(program, 'uOpaque'), + uHalfSize: gl.getUniformLocation(program, 'uHalfSize'), + uSize: gl.getUniformLocation(program, 'uSize'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1fv(uniformLocations.uMatrix, this.matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + opaque: this.opaque, + matrix: this.matrix + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + */ + fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Grayscale image filter class + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Grayscale', + + fragmentSource: { + average: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float average = (color.r + color.b + color.g) / 3.0;\n' + + 'gl_FragColor = vec4(average, average, average, color.a);\n' + + '}', + lightness: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + luminosity: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + }, + + + /** + * Grayscale mode, between 'average', 'lightness', 'luminosity' + * @param {String} type + * @default + */ + mode: 'average', + + mainParameter: 'mode', + + /** + * Apply the Grayscale operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length, value, + mode = this.mode; + for (i = 0; i < len; i += 4) { + if (mode === 'average') { + value = (data[i] + data[i + 1] + data[i + 2]) / 3; + } + else if (mode === 'lightness') { + value = (Math.min(data[i], data[i + 1], data[i + 2]) + + Math.max(data[i], data[i + 1], data[i + 2])) / 2; + } + else if (mode === 'luminosity') { + value = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2]; + } + data[i] = value; + data[i + 1] = value; + data[i + 2] = value; + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var shaderSource = this.fragmentSource[this.mode]; + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMode: gl.getUniformLocation(program, 'uMode'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + // default average mode. + var mode = 1; + gl.uniform1i(uniformLocations.uMode, mode); + }, + + /** + * Grayscale filter isNeutralState implementation + * The filter is never neutral + * on the image + **/ + isNeutralState: function() { + return false; + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + */ + fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Invert filter class + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Invert = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Invert', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uInvert;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'if (uInvert == 1) {\n' + + 'gl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n' + + '} else {\n' + + 'gl_FragColor = color;\n' + + '}\n' + + '}', + + /** + * Filter invert. if false, does nothing + * @param {Boolean} invert + * @default + */ + invert: true, + + mainParameter: 'invert', + + /** + * Apply the Invert operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length; + for (i = 0; i < len; i += 4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + }, + + /** + * Invert filter isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function() { + return !this.invert; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uInvert: gl.getUniformLocation(program, 'uInvert'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1i(uniformLocations.uInvert, this.invert); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + */ + fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; + + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Noise filter class + * @class fabric.Image.filters.Noise + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Noise', + + /** + * Fragment source for the noise program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uStepH;\n' + + 'uniform float uNoise;\n' + + 'uniform float uSeed;\n' + + 'varying vec2 vTexCoord;\n' + + 'float rand(vec2 co, float seed, float vScale) {\n' + + 'return fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'noise', + + /** + * Noise value, from + * @param {Number} noise + * @default + */ + noise: 0, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.noise === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + noise = this.noise, rand; + + for (i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uNoise: gl.getUniformLocation(program, 'uNoise'), + uSeed: gl.getUniformLocation(program, 'uSeed'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uNoise, this.noise / 255); + gl.uniform1f(uniformLocations.uSeed, Math.random()); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + noise: this.noise + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + */ + fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Pixelate filter class + * @class fabric.Image.filters.Pixelate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Pixelate', + + blocksize: 4, + + mainParameter: 'blocksize', + + /** + * Fragment source for the Pixelate program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBlocksize;\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'float blockW = uBlocksize * uStepW;\n' + + 'float blockH = uBlocksize * uStepW;\n' + + 'int posX = int(vTexCoord.x / blockW);\n' + + 'int posY = int(vTexCoord.y / blockH);\n' + + 'float fposX = float(posX);\n' + + 'float fposY = float(posY);\n' + + 'vec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\n' + + 'vec4 color = texture2D(uTexture, squareCoords);\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Apply the Pixelate operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = imageData.height, + jLen = imageData.width, + index, i, j, r, g, b, a, + _i, _j, _iLen, _jLen; + + for (i = 0; i < iLen; i += this.blocksize) { + for (j = 0; j < jLen; j += this.blocksize) { + + index = (i * 4) * jLen + (j * 4); + + r = data[index]; + g = data[index + 1]; + b = data[index + 2]; + a = data[index + 3]; + + _iLen = Math.min(i + this.blocksize, iLen); + _jLen = Math.min(j + this.blocksize, jLen); + for (_i = i; _i < _iLen; _i++) { + for (_j = j; _j < _jLen; _j++) { + index = (_i * 4) * jLen + (_j * 4); + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = a; + } + } + } + } + }, + + /** + * Indicate when the filter is not gonna apply changes to the image + **/ + isNeutralState: function() { + return this.blocksize === 1; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBlocksize: gl.getUniformLocation(program, 'uBlocksize'), + uStepW: gl.getUniformLocation(program, 'uStepW'), + uStepH: gl.getUniformLocation(program, 'uStepH'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBlocksize, this.blocksize); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + */ + fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Remove white filter class + * @class fabric.Image.filters.RemoveColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveColor#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveColor({ + * threshold: 0.2, + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'RemoveColor', + + /** + * Color to remove, in any format understood by fabric.Color. + * @param {String} type + * @default + */ + color: '#FFFFFF', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uLow;\n' + + 'uniform vec4 uHigh;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + 'if(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\n' + + 'gl_FragColor.a = 0.0;\n' + + '}\n' + + '}', + + /** + * distance to actual color, as value up or down from each r,g,b + * between 0 and 1 + **/ + distance: 0.02, + + /** + * For color to remove inside distance, use alpha channel for a smoother deletion + * NOT IMPLEMENTED YET + **/ + useAlpha: false, + + /** + * Constructor + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + * @param {Number} [options.color=#RRGGBB] Threshold value + * @param {Number} [options.distance=10] Distance value + */ + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + distance = this.distance * 255, + r, g, b, + source = new fabric.Color(this.color).getSource(), + lowC = [ + source[0] - distance, + source[1] - distance, + source[2] - distance, + ], + highC = [ + source[0] + distance, + source[1] + distance, + source[2] + distance, + ]; + + + for (i = 0; i < data.length; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + if (r > lowC[0] && + g > lowC[1] && + b > lowC[2] && + r < highC[0] && + g < highC[1] && + b < highC[2]) { + data[i + 3] = 0; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uLow: gl.getUniformLocation(program, 'uLow'), + uHigh: gl.getUniformLocation(program, 'uHigh'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(), + distance = parseFloat(this.distance), + lowC = [ + 0 + source[0] / 255 - distance, + 0 + source[1] / 255 - distance, + 0 + source[2] / 255 - distance, + 1 + ], + highC = [ + source[0] / 255 + distance, + source[1] / 255 + distance, + source[2] / 255 + distance, + 1 + ]; + gl.uniform4fv(uniformLocations.uLow, lowC); + gl.uniform4fv(uniformLocations.uHigh, highC); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + color: this.color, + distance: this.distance + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite + */ + fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + var matrices = { + Brownie: [ + 0.59970,0.34553,-0.27082,0,0.186, + -0.03770,0.86095,0.15059,0,-0.1449, + 0.24113,-0.07441,0.44972,0,-0.02965, + 0,0,0,1,0 + ], + Vintage: [ + 0.62793,0.32021,-0.03965,0,0.03784, + 0.02578,0.64411,0.03259,0,0.02926, + 0.04660,-0.08512,0.52416,0,0.02023, + 0,0,0,1,0 + ], + Kodachrome: [ + 1.12855,-0.39673,-0.03992,0,0.24991, + -0.16404,1.08352,-0.05498,0,0.09698, + -0.16786,-0.56034,1.60148,0,0.13972, + 0,0,0,1,0 + ], + Technicolor: [ + 1.91252,-0.85453,-0.09155,0,0.04624, + -0.30878,1.76589,-0.10601,0,-0.27589, + -0.23110,-0.75018,1.84759,0,0.12137, + 0,0,0,1,0 + ], + Polaroid: [ + 1.438,-0.062,-0.062,0,0, + -0.122,1.378,-0.122,0,0, + -0.016,-0.016,1.483,0,0, + 0,0,0,1,0 + ], + Sepia: [ + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0 + ], + BlackWhite: [ + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 0, 0, 0, 1, 0, + ] + }; + + for (var key in matrices) { + filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: key, + + /** + * Colormatrix for the effect + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: matrices[key], + + /** + * Lock the matrix export for this kind of static, parameter less filters. + */ + mainParameter: false, + /** + * Lock the colormatrix on the color part, skipping alpha + */ + colorsOnly: true, + + }); + fabric.Image.filters[key].fromObject = fabric.Image.filters.BaseFilter.fromObject; + } +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Blend filter class + * @class fabric.Image.filter.BlendColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { + type: 'BlendColor', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + color: '#F95C63', + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend color operation. + **/ + alpha: 1, + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'gl_FragColor.rgb *= uColor.rgb;\n', + screen: 'gl_FragColor.rgb = 1.0 - (1.0 - gl_FragColor.rgb) * (1.0 - uColor.rgb);\n', + add: 'gl_FragColor.rgb += uColor.rgb;\n', + diff: 'gl_FragColor.rgb = abs(gl_FragColor.rgb - uColor.rgb);\n', + subtract: 'gl_FragColor.rgb -= uColor.rgb;\n', + lighten: 'gl_FragColor.rgb = max(gl_FragColor.rgb, uColor.rgb);\n', + darken: 'gl_FragColor.rgb = min(gl_FragColor.rgb, uColor.rgb);\n', + exclusion: 'gl_FragColor.rgb += uColor.rgb - 2.0 * (uColor.rgb * gl_FragColor.rgb);\n', + overlay: 'if (uColor.r < 0.5) {\n' + + 'gl_FragColor.r *= 2.0 * uColor.r;\n' + + '} else {\n' + + 'gl_FragColor.r = 1.0 - 2.0 * (1.0 - gl_FragColor.r) * (1.0 - uColor.r);\n' + + '}\n' + + 'if (uColor.g < 0.5) {\n' + + 'gl_FragColor.g *= 2.0 * uColor.g;\n' + + '} else {\n' + + 'gl_FragColor.g = 1.0 - 2.0 * (1.0 - gl_FragColor.g) * (1.0 - uColor.g);\n' + + '}\n' + + 'if (uColor.b < 0.5) {\n' + + 'gl_FragColor.b *= 2.0 * uColor.b;\n' + + '} else {\n' + + 'gl_FragColor.b = 1.0 - 2.0 * (1.0 - gl_FragColor.b) * (1.0 - uColor.b);\n' + + '}\n', + tint: 'gl_FragColor.rgb *= (1.0 - uColor.a);\n' + + 'gl_FragColor.rgb += uColor.rgb;\n', + }, + + /** + * build the fragment source for the filters, joining the common part with + * the specific one. + * @param {String} mode the mode of the filter, a key of this.fragmentSource + * @return {String} the source to be compiled + * @private + */ + buildSource: function(mode) { + return 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'gl_FragColor = color;\n' + + 'if (color.a > 0.0) {\n' + + this.fragmentSource[mode] + + '}\n' + + '}'; + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode, shaderSource; + if (!options.programCache.hasOwnProperty(cacheKey)) { + shaderSource = this.buildSource(this.mode); + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, iLen = data.length, + tr, tg, tb, + r, g, b, + source, alpha1 = 1 - this.alpha; + + source = new fabric.Color(this.color).getSource(); + tr = source[0] * this.alpha; + tg = source[1] * this.alpha; + tb = source[2] * this.alpha; + + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + break; + case 'screen': + data[i] = 255 - (255 - r) * (255 - tr) / 255; + data[i + 1] = 255 - (255 - g) * (255 - tg) / 255; + data[i + 2] = 255 - (255 - b) * (255 - tb) / 255; + break; + case 'add': + data[i] = r + tr; + data[i + 1] = g + tg; + data[i + 2] = b + tb; + break; + case 'diff': + case 'difference': + data[i] = Math.abs(r - tr); + data[i + 1] = Math.abs(g - tg); + data[i + 2] = Math.abs(b - tb); + break; + case 'subtract': + data[i] = r - tr; + data[i + 1] = g - tg; + data[i + 2] = b - tb; + break; + case 'darken': + data[i] = Math.min(r, tr); + data[i + 1] = Math.min(g, tg); + data[i + 2] = Math.min(b, tb); + break; + case 'lighten': + data[i] = Math.max(r, tr); + data[i + 1] = Math.max(g, tg); + data[i + 2] = Math.max(b, tb); + break; + case 'overlay': + data[i] = tr < 128 ? (2 * r * tr / 255) : (255 - 2 * (255 - r) * (255 - tr) / 255); + data[i + 1] = tg < 128 ? (2 * g * tg / 255) : (255 - 2 * (255 - g) * (255 - tg) / 255); + data[i + 2] = tb < 128 ? (2 * b * tb / 255) : (255 - 2 * (255 - b) * (255 - tb) / 255); + break; + case 'exclusion': + data[i] = tr + r - ((2 * tr * r) / 255); + data[i + 1] = tg + g - ((2 * tg * g) / 255); + data[i + 2] = tb + b - ((2 * tb * b) / 255); + break; + case 'tint': + data[i] = tr + r * alpha1; + data[i + 1] = tg + g * alpha1; + data[i + 2] = tb + b * alpha1; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColor: gl.getUniformLocation(program, 'uColor'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(); + source[0] = this.alpha * source[0] / 255; + source[1] = this.alpha * source[1] / 255; + source[2] = this.alpha * source[2] / 255; + source[3] = this.alpha; + gl.uniform4fv(uniformLocations.uColor, source); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + color: this.color, + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor + */ + fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Image Blend filter class + * @class fabric.Image.filter.BlendImage + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendImage = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.BlendImage.prototype */ { + type: 'BlendImage', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + image: null, + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend image operation. + * not implemented. + **/ + alpha: 1, + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'uniform mat3 uTransformMatrix;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'vTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.rgba *= color2.rgba;\n' + + 'gl_FragColor = color;\n' + + '}', + mask: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.a = color2.a;\n' + + 'gl_FragColor = color;\n' + + '}', + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + var shaderSource = this.fragmentSource[this.mode]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + applyToWebGL: function(options) { + // load texture to blend. + var gl = options.context, + texture = this.createTexture(options.filterBackend, this.image); + this.bindAdditionalTexture(gl, texture, gl.TEXTURE1); + this.callSuper('applyToWebGL', options); + this.unbindAdditionalTexture(gl, gl.TEXTURE1); + }, + + createTexture: function(backend, image) { + return backend.getCachedTexture(image.cacheKey, image._element); + }, + + /** + * Calculate a transformMatrix to adapt the image to blend over + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + calculateMatrix: function() { + var image = this.image, + width = image._element.width, + height = image._element.height; + return [ + 1 / image.scaleX, 0, 0, + 0, 1 / image.scaleY, 0, + -image.left / width, -image.top / height, 1 + ]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + resources = options.filterBackend.resources, + data = imageData.data, iLen = data.length, + width = imageData.width, + height = imageData.height, + tr, tg, tb, ta, + r, g, b, a, + canvas1, context, image = this.image, blendData; + + if (!resources.blendImage) { + resources.blendImage = fabric.util.createCanvasElement(); + } + canvas1 = resources.blendImage; + context = canvas1.getContext('2d'); + if (canvas1.width !== width || canvas1.height !== height) { + canvas1.width = width; + canvas1.height = height; + } + else { + context.clearRect(0, 0, width, height); + } + context.setTransform(image.scaleX, 0, 0, image.scaleY, image.left, image.top); + context.drawImage(image._element, 0, 0, width, height); + blendData = context.getImageData(0, 0, width, height).data; + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + + tr = blendData[i]; + tg = blendData[i + 1]; + tb = blendData[i + 2]; + ta = blendData[i + 3]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + data[i + 3] = a * ta / 255; + break; + case 'mask': + data[i + 3] = ta; + break; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uTransformMatrix: gl.getUniformLocation(program, 'uTransformMatrix'), + uImage: gl.getUniformLocation(program, 'uImage'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var matrix = this.calculateMatrix(); + gl.uniform1i(uniformLocations.uImage, 1); // texture unit 1. + gl.uniformMatrix3fv(uniformLocations.uTransformMatrix, false, matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + image: this.image && this.image.toObject(), + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} callback to be invoked after filter creation + * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage + */ + fabric.Image.filters.BlendImage.fromObject = function(object, callback) { + fabric.Image.fromObject(object.image, function(image) { + var options = fabric.util.object.clone(object); + options.image = image; + callback(new fabric.Image.filters.BlendImage(options)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, + sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin, + ceil = Math.ceil, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Resize image filter class + * @class fabric.Image.filters.Resize + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Resize(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Resize', + + /** + * Resize type + * for webgl resizeType is just lanczos, for canvas2d can be: + * bilinear, hermite, sliceHack, lanczos. + * @param {String} resizeType + * @default + */ + resizeType: 'hermite', + + /** + * Scale factor for resizing, x axis + * @param {Number} scaleX + * @default + */ + scaleX: 1, + + /** + * Scale factor for resizing, y axis + * @param {Number} scaleY + * @default + */ + scaleY: 1, + + /** + * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos + * @param {Number} lanczosLobes + * @default + */ + lanczosLobes: 3, + + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uDelta: gl.getUniformLocation(program, 'uDelta'), + uTaps: gl.getUniformLocation(program, 'uTaps'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform2fv(uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height]); + gl.uniform1fv(uniformLocations.uTaps, this.taps); + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var filterWindow = this.getFilterWindow(), cacheKey = this.type + '_' + filterWindow; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var fragmentShader = this.generateShader(filterWindow); + options.programCache[cacheKey] = this.createProgram(options.context, fragmentShader); + } + return options.programCache[cacheKey]; + }, + + getFilterWindow: function() { + var scale = this.tempScale; + return Math.ceil(this.lanczosLobes / scale); + }, + + getTaps: function() { + var lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, + filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); + for (var i = 1; i <= filterWindow; i++) { + taps[i - 1] = lobeFunction(i * scale); + } + return taps; + }, + + /** + * Generate vertex and shader sources from the necessary steps numbers + * @param {Number} filterWindow + */ + generateShader: function(filterWindow) { + var offsets = new Array(filterWindow), + fragmentShader = this.fragmentSourceTOP, filterWindow; + + for (var i = 1; i <= filterWindow; i++) { + offsets[i - 1] = i + '.0 * uDelta'; + } + + fragmentShader += 'uniform float uTaps[' + filterWindow + '];\n'; + fragmentShader += 'void main() {\n'; + fragmentShader += ' vec4 color = texture2D(uTexture, vTexCoord);\n'; + fragmentShader += ' float sum = 1.0;\n'; + + offsets.forEach(function(offset, i) { + fragmentShader += ' color += texture2D(uTexture, vTexCoord + ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' color += texture2D(uTexture, vTexCoord - ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' sum += 2.0 * uTaps[' + i + '];\n'; + }); + fragmentShader += ' gl_FragColor = color / sum;\n'; + fragmentShader += '}'; + return fragmentShader; + }, + + fragmentSourceTOP: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n', + + /** + * Apply the resize filter to the image + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + options.passes++; + this.width = options.sourceWidth; + this.horizontal = true; + this.dW = Math.round(this.width * this.scaleX); + this.dH = options.sourceHeight; + this.tempScale = this.dW / this.width; + this.taps = this.getTaps(); + options.destinationWidth = this.dW; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceWidth = options.destinationWidth; + + this.height = options.sourceHeight; + this.horizontal = false; + this.dH = Math.round(this.height * this.scaleY); + this.tempScale = this.dH / this.height; + this.taps = this.getTaps(); + options.destinationHeight = this.dH; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceHeight = options.destinationHeight; + } + else { + this.applyTo2d(options); + } + }, + + isNeutralState: function() { + return this.scaleX === 1 && this.scaleY === 1; + }, + + lanczosCreate: function(lobes) { + return function(x) { + if (x >= lobes || x <= -lobes) { + return 0.0; + } + if (x < 1.19209290E-07 && x > -1.19209290E-07) { + return 1.0; + } + x *= Math.PI; + var xx = x / lobes; + return (sin(x) / x) * sin(xx) / xx; + }; + }, + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Resize.prototype + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} scaleX + * @param {Number} scaleY + */ + applyTo2d: function(options) { + var imageData = options.imageData, + scaleX = this.scaleX, + scaleY = this.scaleY; + + this.rcpScaleX = 1 / scaleX; + this.rcpScaleY = 1 / scaleY; + + var oW = imageData.width, oH = imageData.height, + dW = round(oW * scaleX), dH = round(oH * scaleY), + newData; + + if (this.resizeType === 'sliceHack') { + newData = this.sliceByTwo(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'hermite') { + newData = this.hermiteFastResize(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'bilinear') { + newData = this.bilinearFiltering(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'lanczos') { + newData = this.lanczosResize(options, oW, oH, dW, dH); + } + options.imageData = newData; + }, + + /** + * Filter sliceByTwo + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + sliceByTwo: function(options, oW, oH, dW, dH) { + var imageData = options.imageData, + mult = 0.5, doneW = false, doneH = false, stepW = oW * mult, + stepH = oH * mult, resources = fabric.filterBackend.resources, + tmpCanvas, ctx, sX = 0, sY = 0, dX = oW, dY = 0; + if (!resources.sliceByTwo) { + resources.sliceByTwo = document.createElement('canvas'); + } + tmpCanvas = resources.sliceByTwo; + if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) { + tmpCanvas.width = oW * 1.5; + tmpCanvas.height = oH; + } + ctx = tmpCanvas.getContext('2d'); + ctx.clearRect(0, 0, oW * 1.5, oH); + ctx.putImageData(imageData, 0, 0); + + dW = floor(dW); + dH = floor(dH); + + while (!doneW || !doneH) { + oW = stepW; + oH = stepH; + if (dW < floor(stepW * mult)) { + stepW = floor(stepW * mult); + } + else { + stepW = dW; + doneW = true; + } + if (dH < floor(stepH * mult)) { + stepH = floor(stepH * mult); + } + else { + stepH = dH; + doneH = true; + } + ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); + sX = dX; + sY = dY; + dY += stepH; + } + return ctx.getImageData(sX, sY, dW, dH); + }, + + /** + * Filter lanczosResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + lanczosResize: function(options, oW, oH, dW, dH) { + + function process(u) { + var v, i, weight, idx, a, red, green, + blue, alpha, fX, fY; + center.x = (u + 0.5) * ratioX; + icenter.x = floor(center.x); + for (v = 0; v < dH; v++) { + center.y = (v + 0.5) * ratioY; + icenter.y = floor(center.y); + a = 0; red = 0; green = 0; blue = 0; alpha = 0; + for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { + if (i < 0 || i >= oW) { + continue; + } + fX = floor(1000 * abs(i - center.x)); + if (!cacheLanc[fX]) { + cacheLanc[fX] = { }; + } + for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { + if (j < 0 || j >= oH) { + continue; + } + fY = floor(1000 * abs(j - center.y)); + if (!cacheLanc[fX][fY]) { + cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); + } + weight = cacheLanc[fX][fY]; + if (weight > 0) { + idx = (j * oW + i) * 4; + a += weight; + red += weight * srcData[idx]; + green += weight * srcData[idx + 1]; + blue += weight * srcData[idx + 2]; + alpha += weight * srcData[idx + 3]; + } + } + } + idx = (v * dW + u) * 4; + destData[idx] = red / a; + destData[idx + 1] = green / a; + destData[idx + 2] = blue / a; + destData[idx + 3] = alpha / a; + } + + if (++u < dW) { + return process(u); + } + else { + return destImg; + } + } + + var srcData = options.imageData.data, + destImg = options.ctx.createImageData(dW, dH), + destData = destImg.data, + lanczos = this.lanczosCreate(this.lanczosLobes), + ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, + rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, + range2X = ceil(ratioX * this.lanczosLobes / 2), + range2Y = ceil(ratioY * this.lanczosLobes / 2), + cacheLanc = { }, center = { }, icenter = { }; + + return process(0); + }, + + /** + * bilinearFiltering + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + bilinearFiltering: function(options, oW, oH, dW, dH) { + var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, + color, offset = 0, origPix, ratioX = this.rcpScaleX, + ratioY = this.rcpScaleY, + w4 = 4 * (oW - 1), img = options.imageData, + pixels = img.data, destImage = options.ctx.createImageData(dW, dH), + destPixels = destImage.data; + for (i = 0; i < dH; i++) { + for (j = 0; j < dW; j++) { + x = floor(ratioX * j); + y = floor(ratioY * i); + xDiff = ratioX * j - x; + yDiff = ratioY * i - y; + origPix = 4 * (y * oW + x); + + for (chnl = 0; chnl < 4; chnl++) { + a = pixels[origPix + chnl]; + b = pixels[origPix + 4 + chnl]; + c = pixels[origPix + w4 + chnl]; + d = pixels[origPix + w4 + 4 + chnl]; + color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; + destPixels[offset++] = color; + } + } + } + return destImage; + }, + + /** + * hermiteFastResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + hermiteFastResize: function(options, oW, oH, dW, dH) { + var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, + ratioWHalf = ceil(ratioW / 2), + ratioHHalf = ceil(ratioH / 2), + img = options.imageData, data = img.data, + img2 = options.ctx.createImageData(dW, dH), data2 = img2.data; + for (var j = 0; j < dH; j++) { + for (var i = 0; i < dW; i++) { + var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, + gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; + for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { + var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, + centerX = (i + 0.5) * ratioW, w0 = dy * dy; + for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { + var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, + w = sqrt(w0 + dx * dx); + /* eslint-disable max-depth */ + if (w > 1 && w < -1) { + continue; + } + //hermite filter + weight = 2 * w * w * w - 3 * w * w + 1; + if (weight > 0) { + dx = 4 * (xx + yy * oW); + //alpha + gxA += weight * data[dx + 3]; + weightsAlpha += weight; + //colors + if (data[dx + 3] < 255) { + weight = weight * data[dx + 3] / 250; + } + gxR += weight * data[dx]; + gxG += weight * data[dx + 1]; + gxB += weight * data[dx + 2]; + weights += weight; + } + /* eslint-enable max-depth */ + } + } + data2[x2] = gxR / weights; + data2[x2 + 1] = gxG / weights; + data2[x2 + 2] = gxB / weights; + data2[x2 + 3] = gxA / weightsAlpha; + } + } + return img2; + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + scaleX: this.scaleX, + scaleY: this.scaleY, + resizeType: this.resizeType, + lanczosLobes: this.lanczosLobes + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + */ + fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Contrast filter class + * @class fabric.Image.filters.Contrast + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Contrast({ + * contrast: 0.25 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Contrast', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uContrast;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\n' + + 'color.rgb = contrastF * (color.rgb - 0.5) + 0.5;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * contrast value, range from -1 to 1. + * @param {Number} contrast + * @default 0 + */ + contrast: 0, + + mainParameter: 'contrast', + + /** + * Constructor + * @memberOf fabric.Image.filters.Contrast.prototype + * @param {Object} [options] Options object + * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1) + */ + + /** + * Apply the Contrast operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + if (this.contrast === 0) { + return; + } + var imageData = options.imageData, i, len, + data = imageData.data, len = data.length, + contrast = Math.floor(this.contrast * 255), + contrastF = 259 * (contrast + 255) / (255 * (259 - contrast)); + + for (i = 0; i < len; i += 4) { + data[i] = contrastF * (data[i] - 128) + 128; + data[i + 1] = contrastF * (data[i + 1] - 128) + 128; + data[i + 2] = contrastF * (data[i + 2] - 128) + 128; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uContrast: gl.getUniformLocation(program, 'uContrast'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uContrast, this.contrast); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + */ + fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Saturate filter class + * @class fabric.Image.filters.Saturation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Saturation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Saturation({ + * saturation: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Saturation', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uSaturation;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float rgMax = max(color.r, color.g);\n' + + 'float rgbMax = max(rgMax, color.b);\n' + + 'color.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\n' + + 'color.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\n' + + 'color.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Saturation value, from -1 to 1. + * Increases/decreases the color saturation. + * A value of 0 has no effect. + * + * @param {Number} saturation + * @default + */ + saturation: 0, + + mainParameter: 'saturation', + + /** + * Constructor + * @memberOf fabric.Image.filters.Saturate.prototype + * @param {Object} [options] Options object + * @param {Number} [options.saturate=0] Value to saturate the image (-1...1) + */ + + /** + * Apply the Saturation operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.saturation === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.saturation, i, max; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + data[i] += max !== data[i] ? (max - data[i]) * adjust : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uSaturation: gl.getUniformLocation(program, 'uSaturation'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uSaturation, -this.saturation); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate + */ + fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Vibrance filter class + * @class fabric.Image.filters.Vibrance + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Vibrance#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Vibrance({ + * vibrance: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Vibrance = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Vibrance.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Vibrance', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uVibrance;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float max = max(color.r, max(color.g, color.b));\n' + + 'float avg = (color.r + color.g + color.b) / 3.0;\n' + + 'float amt = (abs(max - avg) * 2.0) * uVibrance;\n' + + 'color.r += max != color.r ? (max - color.r) * amt : 0.00;\n' + + 'color.g += max != color.g ? (max - color.g) * amt : 0.00;\n' + + 'color.b += max != color.b ? (max - color.b) * amt : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Vibrance value, from -1 to 1. + * Increases/decreases the saturation of more muted colors with less effect on saturated colors. + * A value of 0 has no effect. + * + * @param {Number} vibrance + * @default + */ + vibrance: 0, + + mainParameter: 'vibrance', + + /** + * Constructor + * @memberOf fabric.Image.filters.Vibrance.prototype + * @param {Object} [options] Options object + * @param {Number} [options.vibrance=0] Vibrance value for the image (between -1 and 1) + */ + + /** + * Apply the Vibrance operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.vibrance === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.vibrance, i, max, avg, amt; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + amt = ((Math.abs(max - avg) * 2 / 255) * adjust); + data[i] += max !== data[i] ? (max - data[i]) * amt : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * amt : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * amt : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uVibrance: gl.getUniformLocation(program, 'uVibrance'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uVibrance, -this.vibrance); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance + */ + fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Blur filter class + * @class fabric.Image.filters.Blur + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Blur#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Blur({ + * blur: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Blur = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blur.prototype */ { + + type: 'Blur', + + /* +'gl_FragColor = vec4(0.0);', +'gl_FragColor += texture2D(texture, vTexCoord + -7 * uDelta)*0.0044299121055113265;', +'gl_FragColor += texture2D(texture, vTexCoord + -6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + -5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + -4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + -3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + -2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + -1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord )*0.159576912161;', +'gl_FragColor += texture2D(texture, vTexCoord + 1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord + 2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + 3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + 4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + 5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + 6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + 7 * uDelta)*0.0044299121055113265;', +*/ + + /* eslint-disable max-len */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n' + + 'const float nSamples = 15.0;\n' + + 'vec3 v3offset = vec3(12.9898, 78.233, 151.7182);\n' + + 'float random(vec3 scale) {\n' + + /* use the fragment position for a different seed per-pixel */ + 'return fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = vec4(0.0);\n' + + 'float total = 0.0;\n' + + 'float offset = random(v3offset);\n' + + 'for (float t = -nSamples; t <= nSamples; t++) {\n' + + 'float percent = (t + offset - 0.5) / nSamples;\n' + + 'float weight = 1.0 - abs(percent);\n' + + 'color += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\n' + + 'total += weight;\n' + + '}\n' + + 'gl_FragColor = color / total;\n' + + '}', + /* eslint-enable max-len */ + + /** + * blur value, in percentage of image dimensions. + * specific to keep the image blur constant at different resolutions + * range between 0 and 1. + */ + blur: 0, + + mainParameter: 'blur', + + applyTo: function(options) { + if (options.webgl) { + // this aspectRatio is used to give the same blur to vertical and horizontal + this.aspectRatio = options.sourceWidth / options.sourceHeight; + options.passes++; + this._setupFrameBuffer(options); + this.horizontal = true; + this.applyToWebGL(options); + this._swapTextures(options); + this._setupFrameBuffer(options); + this.horizontal = false; + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + applyTo2d: function(options) { + // paint canvasEl with current image data. + //options.ctx.putImageData(options.imageData, 0, 0); + options.imageData = this.simpleBlur(options); + }, + + simpleBlur: function(options) { + var resources = options.filterBackend.resources, canvas1, canvas2, + width = options.imageData.width, + height = options.imageData.height; + + if (!resources.blurLayer1) { + resources.blurLayer1 = fabric.util.createCanvasElement(); + resources.blurLayer2 = fabric.util.createCanvasElement(); + } + canvas1 = resources.blurLayer1; + canvas2 = resources.blurLayer2; + if (canvas1.width !== width || canvas1.height !== height) { + canvas2.width = canvas1.width = width; + canvas2.height = canvas1.height = height; + } + var ctx1 = canvas1.getContext('2d'), + ctx2 = canvas2.getContext('2d'), + nSamples = 15, + random, percent, j, i, + blur = this.blur * 0.06 * 0.5; + + // load first canvas + ctx1.putImageData(options.imageData, 0, 0); + ctx2.clearRect(0, 0, width, height); + + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * width + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, j, random); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * height + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, random, j); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + options.ctx.drawImage(canvas1, 0, 0); + var newImageData = options.ctx.getImageData(0, 0, canvas1.width, canvas1.height); + ctx1.globalAlpha = 1; + ctx1.clearRect(0, 0, canvas1.width, canvas1.height); + return newImageData; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + delta: gl.getUniformLocation(program, 'uDelta'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var delta = this.chooseRightDelta(); + gl.uniform2fv(uniformLocations.delta, delta); + }, + + /** + * choose right value of image percentage to blur with + * @returns {Array} a numeric array with delta values + */ + chooseRightDelta: function() { + var blurScale = 1, delta = [0, 0], blur; + if (this.horizontal) { + if (this.aspectRatio > 1) { + // image is wide, i want to shrink radius horizontal + blurScale = 1 / this.aspectRatio; + } + } + else { + if (this.aspectRatio < 1) { + // image is tall, i want to shrink radius vertical + blurScale = this.aspectRatio; + } + } + blur = blurScale * this.blur * 0.12; + if (this.horizontal) { + delta[0] = blur; + } + else { + delta[1] = blur; + } + return delta; + }, + }); + + /** + * Deserialize a JSON definition of a BlurFilter into a concrete instance. + */ + filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Gamma filter class + * @class fabric.Image.filters.Gamma + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Gamma#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Gamma({ + * gamma: [1, 0.5, 2.1] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Gamma = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Gamma.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Gamma', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec3 uGamma;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec3 correction = (1.0 / uGamma);\n' + + 'color.r = pow(color.r, correction.r);\n' + + 'color.g = pow(color.g, correction.g);\n' + + 'color.b = pow(color.b, correction.b);\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.rgb *= color.a;\n' + + '}', + + /** + * Gamma array value, from 0.01 to 2.2. + * @param {Array} gamma + * @default + */ + gamma: [1, 1, 1], + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'gamma', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.gamma = [1, 1, 1]; + filters.BaseFilter.prototype.initialize.call(this, options); + }, + + /** + * Apply the Gamma operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, data = imageData.data, + gamma = this.gamma, len = data.length, + rInv = 1 / gamma[0], gInv = 1 / gamma[1], + bInv = 1 / gamma[2], i; + + if (!this.rVals) { + // eslint-disable-next-line + this.rVals = new Uint8Array(256); + // eslint-disable-next-line + this.gVals = new Uint8Array(256); + // eslint-disable-next-line + this.bVals = new Uint8Array(256); + } + + // This is an optimization - pre-compute a look-up table for each color channel + // instead of performing these pow calls for each pixel in the image. + for (i = 0, len = 256; i < len; i++) { + this.rVals[i] = Math.pow(i / 255, rInv) * 255; + this.gVals[i] = Math.pow(i / 255, gInv) * 255; + this.bVals[i] = Math.pow(i / 255, bInv) * 255; + } + for (i = 0, len = data.length; i < len; i += 4) { + data[i] = this.rVals[data[i]]; + data[i + 1] = this.gVals[data[i + 1]]; + data[i + 2] = this.bVals[data[i + 2]]; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uGamma: gl.getUniformLocation(program, 'uGamma'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform3fv(uniformLocations.uGamma, this.gamma); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma + */ + fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * A container class that knows how to apply a sequence of filters to an input image. + */ + filters.Composed = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Composed.prototype */ { + + type: 'Composed', + + /** + * A non sparse array of filters to apply + */ + subFilters: [], + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.subFilters = this.subFilters.slice(0); + }, + + /** + * Apply this container's filters to the input image provided. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be applied. + */ + applyTo: function(options) { + options.passes += this.subFilters.length - 1; + this.subFilters.forEach(function(filter) { + filter.applyTo(options); + }); + }, + + /** + * Serialize this filter into JSON. + * + * @returns {Object} A JSON representation of this filter. + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + subFilters: this.subFilters.map(function(filter) { return filter.toObject(); }), + }); + }, + + isNeutralState: function() { + return !this.subFilters.some(function(filter) { return !filter.isNeutralState(); }); + } + }); + + /** + * Deserialize a JSON definition of a ComposedFilter into a concrete instance. + */ + fabric.Image.filters.Composed.fromObject = function(object, callback) { + var filters = object.subFilters || [], + subFilters = filters.map(function(filter) { + return new fabric.Image.filters[filter.type](filter); + }), + instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); + callback && callback(instance); + return instance; + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * HueRotation filter class + * @class fabric.Image.filters.HueRotation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.HueRotation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.HueRotation({ + * rotation: -0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.HueRotation = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.HueRotation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'HueRotation', + + /** + * HueRotation value, from -1 to 1. + * the unit is radians + * @param {Number} myParameter + * @default + */ + rotation: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'rotation', + + calculateMatrix: function() { + var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad), + aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos; + this.matrix = [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ]; + this.matrix[0] = cos + OneMinusCos / 3; + this.matrix[1] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[2] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[5] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[6] = cos + aThird * OneMinusCos; + this.matrix[7] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[10] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[11] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[12] = cos + aThird * OneMinusCos; + }, + + /** + * HueRotation isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function(options) { + this.calculateMatrix(); + return filters.BaseFilter.prototype.isNeutralState.call(this, options); + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + this.calculateMatrix(); + filters.BaseFilter.prototype.applyTo.call(this, options); + }, + + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation + */ + fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + clone = fabric.util.object.clone; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + + var additionalProps = + ('fontFamily fontWeight fontSize text underline overline linethrough' + + ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles' + + ' direction path pathStartOffset pathSide').split(' '); + + /** + * Text class + * @class fabric.Text + * @extends fabric.Object + * @return {fabric.Text} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} + * @see {@link fabric.Text#initialize} for constructor definition + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { + + /** + * Properties which when set cause object to change dimensions + * @type Array + * @private + */ + _dimensionAffectingProps: [ + 'fontSize', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'text', + 'charSpacing', + 'textAlign', + 'styles', + 'path', + 'pathStartOffset', + 'pathSide' + ], + + /** + * @private + */ + _reNewline: /\r?\n/, + + /** + * Use this regular expression to filter for whitespaces that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpacesAndTabs: /[ \t\r]/g, + + /** + * Use this regular expression to filter for whitespace that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpaceAndTab: /[ \t\r]/, + + /** + * Use this regular expression to filter consecutive groups of non spaces. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reWords: /\S+/g, + + /** + * Type of an object + * @type String + * @default + */ + type: 'text', + + /** + * Font size (in pixels) + * @type Number + * @default + */ + fontSize: 40, + + /** + * Font weight (e.g. bold, normal, 400, 600, 800) + * @type {(Number|String)} + * @default + */ + fontWeight: 'normal', + + /** + * Font family + * @type String + * @default + */ + fontFamily: 'Times New Roman', + + /** + * Text decoration underline. + * @type Boolean + * @default + */ + underline: false, + + /** + * Text decoration overline. + * @type Boolean + * @default + */ + overline: false, + + /** + * Text decoration linethrough. + * @type Boolean + * @default + */ + linethrough: false, + + /** + * Text alignment. Possible values: "left", "center", "right", "justify", + * "justify-left", "justify-center" or "justify-right". + * @type String + * @default + */ + textAlign: 'left', + + /** + * Font style . Possible values: "", "normal", "italic" or "oblique". + * @type String + * @default + */ + fontStyle: 'normal', + + /** + * Line height + * @type Number + * @default + */ + lineHeight: 1.16, + + /** + * Superscript schema object (minimum overlap) + * @type {Object} + * @default + */ + superscript: { + size: 0.60, // fontSize factor + baseline: -0.35 // baseline-shift factor (upwards) + }, + + /** + * Subscript schema object (minimum overlap) + * @type {Object} + * @default + */ + subscript: { + size: 0.60, // fontSize factor + baseline: 0.11 // baseline-shift factor (downwards) + }, + + /** + * Background color of text lines + * @type String + * @default + */ + textBackgroundColor: '', + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps), + + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps), + + /** + * When defined, an object is rendered via stroke and this property specifies its color. + * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 + * @type String + * @default + */ + stroke: null, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * fabric.Path that the text should follow. + * since 4.6.0 the path will be drawn automatically. + * if you want to make the path visible, give it a stroke and strokeWidth or fill value + * if you want it to be hidden, assign visible = false to the path. + * This feature is in BETA, and SVG import/export is not yet supported. + * @type fabric.Path + * @example + * var textPath = new fabric.Text('Text on a path', { + * top: 150, + * left: 150, + * textAlign: 'center', + * charSpacing: -50, + * path: new fabric.Path('M 0 0 C 50 -100 150 -100 200 0', { + * strokeWidth: 1, + * visible: false + * }), + * pathSide: 'left', + * pathStartOffset: 0 + * }); + * @default + */ + path: null, + + /** + * Offset amount for text path starting position + * Only used when text has a path + * @type Number + * @default + */ + pathStartOffset: 0, + + /** + * Which side of the path the text should be drawn on. + * Only used when text has a path + * @type {String} 'left|right' + * @default + */ + pathSide: 'left', + + /** + * @private + */ + _fontSizeFraction: 0.222, + + /** + * @private + */ + offsets: { + underline: 0.10, + linethrough: -0.315, + overline: -0.88 + }, + + /** + * Text Line proportion to font Size (in pixels) + * @type Number + * @default + */ + _fontSizeMult: 1.13, + + /** + * additional space between characters + * expressed in thousands of em unit + * @type Number + * @default + */ + charSpacing: 0, + + /** + * Object containing character styles - top-level properties -> line numbers, + * 2nd-level properties - character numbers + * @type Object + * @default + */ + styles: null, + + /** + * Reference to a context to measure text char or couple of chars + * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas + * once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every + * text object created. + * @type {CanvasRenderingContext2D} + * @default + */ + _measuringContext: null, + + /** + * Baseline shift, styles only, keep at 0 for the main text object + * @type {Number} + * @default + */ + deltaY: 0, + + /** + * WARNING: EXPERIMENTAL. NOT SUPPORTED YET + * determine the direction of the text. + * This has to be set manually together with textAlign and originX for proper + * experience. + * some interesting link for the future + * https://www.w3.org/International/questions/qa-bidi-unicode-controls + * @since 4.5.0 + * @type {String} 'ltr|rtl' + * @default + */ + direction: 'ltr', + + /** + * Array of properties that define a style unit (of 'styles'). + * @type {Array} + * @default + */ + _styleProperties: [ + 'stroke', + 'strokeWidth', + 'fill', + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'underline', + 'overline', + 'linethrough', + 'deltaY', + 'textBackgroundColor', + ], + + /** + * contains characters bounding boxes + */ + __charBounds: [], + + /** + * use this size when measuring text. To avoid IE11 rounding errors + * @type {Number} + * @default + * @readonly + * @private + */ + CACHE_FONT_SIZE: 400, + + /** + * contains the min text width to avoid getting 0 + * @type {Number} + * @default + */ + MIN_TEXT_WIDTH: 2, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.text = text; + this.__skipDimension = true; + this.callSuper('initialize', options); + if (this.path) { + this.setPathInfo(); + } + this.__skipDimension = false; + this.initDimensions(); + this.setCoords(); + this.setupState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * If text has a path, it will add the extra information needed + * for path and text calculations + * @return {fabric.Text} thisArg + */ + setPathInfo: function() { + var path = this.path; + if (path) { + path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path); + } + }, + + /** + * Return a context for measurement of text string. + * if created it gets stored for reuse + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + getMeasuringContext: function() { + // if we did not return we have to measure something. + if (!fabric._measuringContext) { + fabric._measuringContext = this.canvas && this.canvas.contextCache || + fabric.util.createCanvasElement().getContext('2d'); + } + return fabric._measuringContext; + }, + + /** + * @private + * Divides text into lines of text and lines of graphemes. + */ + _splitText: function() { + var newLines = this._splitTextIntoLines(this.text); + this.textLines = newLines.lines; + this._textLines = newLines.graphemeLines; + this._unwrappedTextLines = newLines._unwrappedLines; + this._text = newLines.graphemeText; + return newLines; + }, + + /** + * Initialize or update text dimensions. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this._splitText(); + this._clearCache(); + if (this.path) { + this.width = this.path.width; + this.height = this.path.height; + } + else { + this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; + this.height = this.calcTextHeight(); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Enlarge space boxes and shift the others + */ + enlargeSpaces: function() { + var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { + continue; + } + accumulatedSpace = 0; + line = this._textLines[i]; + currentLineWidth = this.getLineWidth(i); + if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { + numberOfSpaces = spaces.length; + diffSpace = (this.width - currentLineWidth) / numberOfSpaces; + for (var j = 0, jlen = line.length; j <= jlen; j++) { + charBound = this.__charBounds[i][j]; + if (this._reSpaceAndTab.test(line[j])) { + charBound.width += diffSpace; + charBound.kernedWidth += diffSpace; + charBound.left += accumulatedSpace; + accumulatedSpace += diffSpace; + } + else { + charBound.left += accumulatedSpace; + } + } + } + } + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + return lineIndex === this._textLines.length - 1; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * It return always for text and Itext. + * @return Number + */ + missingNewlineOffset: function() { + return 1; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @param {Object} dim.x width of object to be cached + * @param {Object} dim.y height of object to be cached + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var dims = this.callSuper('_getCacheCanvasDimensions'); + var fontSize = this.fontSize; + dims.width += fontSize * dims.zoomX; + dims.height += fontSize * dims.zoomY; + return dims; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var path = this.path; + path && !path.isNotVisible() && path._render(ctx); + this._setTextStyles(ctx); + this._renderTextLinesBackground(ctx); + this._renderTextDecoration(ctx, 'underline'); + this._renderText(ctx); + this._renderTextDecoration(ctx, 'overline'); + this._renderTextDecoration(ctx, 'linethrough'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderTextStroke(ctx); + this._renderTextFill(ctx); + } + else { + this._renderTextFill(ctx); + this._renderTextStroke(ctx); + } + }, + + /** + * Set the font parameter of the context with the object properties or with charStyle + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [charStyle] object with font style properties + * @param {String} [charStyle.fontFamily] Font Family + * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix ) + * @param {String} [charStyle.fontWeight] Font weight + * @param {String} [charStyle.fontStyle] Font style (italic|normal) + */ + _setTextStyles: function(ctx, charStyle, forMeasuring) { + ctx.textBaseline = 'alphabetic'; + ctx.font = this._getFontDeclaration(charStyle, forMeasuring); + }, + + /** + * calculate and return the text Width measuring each line. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {Number} Maximum width of fabric.Text object + */ + calcTextWidth: function() { + var maxWidth = this.getLineWidth(0); + + for (var i = 1, len = this._textLines.length; i < len; i++) { + var currentLineWidth = this.getLineWidth(i); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {String} method Method name ("fillText" or "strokeText") + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Text to render + * @param {Number} left Left position of text + * @param {Number} top Top position of text + * @param {Number} lineIndex Index of a line in a text + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + this._renderChars(method, ctx, line, left, top, lineIndex); + }, + + /** + * Renders the text background for lines, taking care of style + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextLinesBackground: function(ctx) { + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) { + return; + } + var heightOfLine, + lineLeftOffset, originalFill = ctx.fillStyle, + line, lastColor, + leftOffset = this._getLeftOffset(), + lineTopOffset = this._getTopOffset(), + boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path, + drawStart; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) { + lineTopOffset += heightOfLine; + continue; + } + line = this._textLines[i]; + lineLeftOffset = this._getLineLeftOffset(i); + boxWidth = 0; + boxStart = 0; + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillStyle = currentColor; + currentColor && ctx.fillRect( + -charBox.width / 2, + -heightOfLine / this.lineHeight * (1 - this._fontSizeFraction), + charBox.width, + heightOfLine / this.lineHeight + ); + ctx.restore(); + } + else if (currentColor !== lastColor) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = lastColor; + lastColor && ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + if (currentColor && !path) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentColor; + ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + } + lineTopOffset += heightOfLine; + } + ctx.fillStyle = originalFill; + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {Object} decl style declaration for cache + * @param {String} decl.fontFamily fontFamily + * @param {String} decl.fontStyle fontStyle + * @param {String} decl.fontWeight fontWeight + * @return {Object} reference to cache + */ + getFontCache: function(decl) { + var fontFamily = decl.fontFamily.toLowerCase(); + if (!fabric.charWidthsCache[fontFamily]) { + fabric.charWidthsCache[fontFamily] = { }; + } + var cache = fabric.charWidthsCache[fontFamily], + cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase(); + if (!cache[cacheProp]) { + cache[cacheProp] = { }; + } + return cache[cacheProp]; + }, + + /** + * measure and return the width of a single character. + * possibly overridden to accommodate different measure logic or + * to hook some external lib for character measurement + * @private + * @param {String} _char, char to be measured + * @param {Object} charStyle style of char to be measured + * @param {String} [previousChar] previous char + * @param {Object} [prevCharStyle] style of previous char + */ + _measureChar: function(_char, charStyle, previousChar, prevCharStyle) { + // first i try to return from cache + var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle), + previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char, + stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth, + fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth; + + if (previousChar && fontCache[previousChar] !== undefined) { + previousWidth = fontCache[previousChar]; + } + if (fontCache[_char] !== undefined) { + kernedWidth = width = fontCache[_char]; + } + if (stylesAreEqual && fontCache[couple] !== undefined) { + coupleWidth = fontCache[couple]; + kernedWidth = coupleWidth - previousWidth; + } + if (width === undefined || previousWidth === undefined || coupleWidth === undefined) { + var ctx = this.getMeasuringContext(); + // send a TRUE to specify measuring font size CACHE_FONT_SIZE + this._setTextStyles(ctx, charStyle, true); + } + if (width === undefined) { + kernedWidth = width = ctx.measureText(_char).width; + fontCache[_char] = width; + } + if (previousWidth === undefined && stylesAreEqual && previousChar) { + previousWidth = ctx.measureText(previousChar).width; + fontCache[previousChar] = previousWidth; + } + if (stylesAreEqual && coupleWidth === undefined) { + // we can measure the kerning couple and subtract the width of the previous character + coupleWidth = ctx.measureText(couple).width; + fontCache[couple] = coupleWidth; + kernedWidth = coupleWidth - previousWidth; + } + return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier }; + }, + + /** + * Computes height of character at given position + * @param {Number} line the line index number + * @param {Number} _char the character index number + * @return {Number} fontSize of the character + */ + getHeightOfChar: function(line, _char) { + return this.getValueOfPropertyAt(line, _char, 'fontSize'); + }, + + /** + * measure a text line measuring all characters. + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + measureLine: function(lineIndex) { + var lineInfo = this._measureLine(lineIndex); + if (this.charSpacing !== 0) { + lineInfo.width -= this._getWidthOfCharSpacing(); + } + if (lineInfo.width < 0) { + lineInfo.width = 0; + } + return lineInfo; + }, + + /** + * measure every grapheme of a line, populating __charBounds + * @param {Number} lineIndex + * @return {Object} object.width total width of characters + * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs + */ + _measureLine: function(lineIndex) { + var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, + graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length), + positionInPath = 0, startingPoint, totalPathLength, path = this.path, + reverse = this.pathSide === 'right'; + + this.__charBounds[lineIndex] = lineBounds; + for (i = 0; i < line.length; i++) { + grapheme = line[i]; + graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); + lineBounds[i] = graphemeInfo; + width += graphemeInfo.kernedWidth; + prevGrapheme = grapheme; + } + // this latest bound box represent the last character of the line + // to simplify cursor handling in interactive mode. + lineBounds[i] = { + left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, + width: 0, + kernedWidth: 0, + height: this.fontSize + }; + if (path) { + totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length; + startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); + startingPoint.x += path.pathOffset.x; + startingPoint.y += path.pathOffset.y; + switch (this.textAlign) { + case 'left': + positionInPath = reverse ? (totalPathLength - width) : 0; + break; + case 'center': + positionInPath = (totalPathLength - width) / 2; + break; + case 'right': + positionInPath = reverse ? 0 : (totalPathLength - width); + break; + //todo - add support for justify + } + positionInPath += this.pathStartOffset * (reverse ? -1 : 1); + for (i = reverse ? line.length - 1 : 0; + reverse ? i >= 0 : i < line.length; + reverse ? i-- : i++) { + graphemeInfo = lineBounds[i]; + if (positionInPath > totalPathLength) { + positionInPath %= totalPathLength; + } + else if (positionInPath < 0) { + positionInPath += totalPathLength; + } + // it would probably much faster to send all the grapheme position for a line + // and calculate path position/angle at once. + this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint); + positionInPath += graphemeInfo.kernedWidth; + } + } + return { width: width, numOfSpaces: numOfSpaces }; + }, + + /** + * Calculate the angle and the left,top position of the char that follow a path. + * It appends it to graphemeInfo to be reused later at rendering + * @private + * @param {Number} positionInPath to be measured + * @param {Object} graphemeInfo current grapheme box information + * @param {Object} startingPoint position of the point + */ + _setGraphemeOnPath: function(positionInPath, graphemeInfo, startingPoint) { + var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2, + path = this.path; + + // we are at currentPositionOnPath. we want to know what point on the path is. + var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo); + graphemeInfo.renderLeft = info.x - startingPoint.x; + graphemeInfo.renderTop = info.y - startingPoint.y; + graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); + }, + + /** + * Measure and return the info of a single grapheme. + * needs the the info of previous graphemes already filled + * @private + * @param {String} grapheme to be measured + * @param {Number} lineIndex index of the line where the char is + * @param {Number} charIndex position in the line + * @param {String} [prevGrapheme] character preceding the one to be measured + */ + _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { + var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), + prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { }, + info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), + kernedWidth = info.kernedWidth, + width = info.width, charSpacing; + + if (this.charSpacing !== 0) { + charSpacing = this._getWidthOfCharSpacing(); + width += charSpacing; + kernedWidth += charSpacing; + } + + var box = { + width: width, + left: 0, + height: style.fontSize, + kernedWidth: kernedWidth, + deltaY: style.deltaY, + }; + if (charIndex > 0 && !skipLeft) { + var previousBox = this.__charBounds[lineIndex][charIndex - 1]; + box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; + } + return box; + }, + + /** + * Calculate height of line at 'lineIndex' + * @param {Number} lineIndex index of line to calculate + * @return {Number} + */ + getHeightOfLine: function(lineIndex) { + if (this.__lineHeights[lineIndex]) { + return this.__lineHeights[lineIndex]; + } + + var line = this._textLines[lineIndex], + // char 0 is measured before the line cycle because it nneds to char + // emptylines + maxHeight = this.getHeightOfChar(lineIndex, 0); + for (var i = 1, len = line.length; i < len; i++) { + maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); + } + + return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; + }, + + /** + * Calculate text box height + */ + calcTextHeight: function() { + var lineHeight, height = 0; + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineHeight = this.getHeightOfLine(i); + height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); + } + return height; + }, + + /** + * @private + * @return {Number} Left offset + */ + _getLeftOffset: function() { + return this.direction === 'ltr' ? -this.width / 2 : this.width / 2; + }, + + /** + * @private + * @return {Number} Top offset + */ + _getTopOffset: function() { + return -this.height / 2; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} method Method name ("fillText" or "strokeText") + */ + _renderTextCommon: function(ctx, method) { + ctx.save(); + var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); + for (var i = 0, len = this._textLines.length; i < len; i++) { + var heightOfLine = this.getHeightOfLine(i), + maxHeight = heightOfLine / this.lineHeight, + leftOffset = this._getLineLeftOffset(i); + this._renderTextLine( + method, + ctx, + this._textLines[i], + left + leftOffset, + top + lineHeights + maxHeight, + i + ); + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextFill: function(ctx) { + if (!this.fill && !this.styleHas('fill')) { + return; + } + + this._renderTextCommon(ctx, 'fillText'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextStroke: function(ctx) { + if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + this._setLineDash(ctx, this.strokeDashArray); + ctx.beginPath(); + this._renderTextCommon(ctx, 'strokeText'); + ctx.closePath(); + ctx.restore(); + }, + + /** + * @private + * @param {String} method fillText or strokeText. + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} line Content of the line, splitted in an array by grapheme + * @param {Number} left + * @param {Number} top + * @param {Number} lineIndex + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, + boxWidth = 0, + timeToRender, + path = this.path, + shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, + isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, + drawingLeft; + + ctx.save(); + top -= lineHeight * this._fontSizeFraction / this.lineHeight; + if (shortCut) { + // render all the line in one pass without checking + // drawingLeft = isLtr ? left : left - this.getLineWidth(lineIndex); + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight); + ctx.restore(); + return; + } + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing || path; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + left += sign * (charBox.kernedWidth - charBox.width); + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = this._hasStyleChanged(actualStyle, nextStyle); + } + if (timeToRender) { + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight); + ctx.restore(); + } + else { + drawingLeft = left; + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top, lineHeight); + } + charsToRender = ''; + actualStyle = nextStyle; + left += sign * boxWidth; + boxWidth = 0; + } + } + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {fabric.Gradient} filler a fabric gradient instance + * @return {CanvasPattern} a pattern to use as fill/stroke style + */ + _applyPatternGradientTransformText: function(filler) { + var pCanvas = fabric.util.createCanvasElement(), pCtx, + // TODO: verify compatibility with strokeUniform + width = this.width + this.strokeWidth, height = this.height + this.strokeWidth; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.fillStyle = filler.toLive(pCtx); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fill(); + return pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + handleFiller: function(ctx, property, filler) { + var offsetX, offsetY; + if (filler.toLive) { + if (filler.gradientUnits === 'percentage' || filler.gradientTransform || filler.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + offsetX = -this.width / 2; + offsetY = -this.height / 2; + ctx.translate(offsetX, offsetY); + ctx[property] = this._applyPatternGradientTransformText(filler); + return { offsetX: offsetX, offsetY: offsetY }; + } + else { + // is a simple gradient or pattern + ctx[property] = filler.toLive(ctx, this); + return this._applyPatternGradientTransform(ctx, filler); + } + } + else { + // is a color + ctx[property] = filler; + } + return { offsetX: 0, offsetY: 0 }; + }, + + _setStrokeStyles: function(ctx, decl) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = this.strokeLineCap; + ctx.lineDashOffset = this.strokeDashOffset; + ctx.lineJoin = this.strokeLineJoin; + ctx.miterLimit = this.strokeMiterLimit; + return this.handleFiller(ctx, 'strokeStyle', decl.stroke); + }, + + _setFillStyles: function(ctx, decl) { + return this.handleFiller(ctx, 'fillStyle', decl.fill); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {String} _char + * @param {Number} left Left coordinate + * @param {Number} top Top coordinate + * @param {Number} lineHeight Height of the line + */ + _renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) { + var decl = this._getStyleDeclaration(lineIndex, charIndex), + fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), + shouldFill = method === 'fillText' && fullDecl.fill, + shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth, + fillOffsets, strokeOffsets; + + if (!shouldStroke && !shouldFill) { + return; + } + ctx.save(); + + shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl)); + shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl)); + + ctx.font = this._getFontDeclaration(fullDecl); + + + if (decl && decl.textBackgroundColor) { + this._removeShadow(ctx); + } + if (decl && decl.deltaY) { + top += decl.deltaY; + } + shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY); + shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY); + ctx.restore(); + }, + + /** + * Turns the character into a 'superior figure' (i.e. 'superscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSuperscript: function(start, end) { + return this._setScript(start, end, this.superscript); + }, + + /** + * Turns the character into an 'inferior figure' (i.e. 'subscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSubscript: function(start, end) { + return this._setScript(start, end, this.subscript); + }, + + /** + * Applies 'schema' at given position + * @private + * @param {Number} start selection start + * @param {Number} end selection end + * @param {Number} schema + * @returns {fabric.Text} thisArg + * @chainable + */ + _setScript: function(start, end, schema) { + var loc = this.get2DCursorLocation(start, true), + fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'), + dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'), + style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline }; + this.setSelectionStyles(style, start, end); + return this; + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChanged: function(prevStyle, thisStyle) { + return prevStyle.fill !== thisStyle.fill || + prevStyle.stroke !== thisStyle.stroke || + prevStyle.strokeWidth !== thisStyle.strokeWidth || + prevStyle.fontSize !== thisStyle.fontSize || + prevStyle.fontFamily !== thisStyle.fontFamily || + prevStyle.fontWeight !== thisStyle.fontWeight || + prevStyle.fontStyle !== thisStyle.fontStyle || + prevStyle.deltaY !== thisStyle.deltaY; + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChangedForSvg: function(prevStyle, thisStyle) { + return this._hasStyleChanged(prevStyle, thisStyle) || + prevStyle.overline !== thisStyle.overline || + prevStyle.underline !== thisStyle.underline || + prevStyle.linethrough !== thisStyle.linethrough; + }, + + /** + * @private + * @param {Number} lineIndex index text line + * @return {Number} Line left offset + */ + _getLineLeftOffset: function(lineIndex) { + var lineWidth = this.getLineWidth(lineIndex), + lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, + isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); + if (textAlign === 'justify' + || (textAlign === 'justify-center' && !isEndOfWrapping) + || (textAlign === 'justify-right' && !isEndOfWrapping) + || (textAlign === 'justify-left' && !isEndOfWrapping) + ) { + return 0; + } + if (textAlign === 'center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'right') { + leftOffset = lineDiff; + } + if (textAlign === 'justify-center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'justify-right') { + leftOffset = lineDiff; + } + if (direction === 'rtl') { + leftOffset -= lineDiff; + } + return leftOffset; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = []; + this.__lineHeights = []; + this.__charBounds = []; + }, + + /** + * @private + */ + _shouldClearDimensionCache: function() { + var shouldClear = this._forceClearCache; + shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); + if (shouldClear) { + this.dirty = true; + this._forceClearCache = false; + } + return shouldClear; + }, + + /** + * Measure a single line given its index. Used to calculate the initial + * text bounding box. The values are calculated and stored in __lineWidths cache. + * @private + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + getLineWidth: function(lineIndex) { + if (this.__lineWidths[lineIndex]) { + return this.__lineWidths[lineIndex]; + } + + var width, line = this._textLines[lineIndex], lineInfo; + + if (line === '') { + width = 0; + } + else { + lineInfo = this.measureLine(lineIndex); + width = lineInfo.width; + } + this.__lineWidths[lineIndex] = width; + return width; + }, + + _getWidthOfCharSpacing: function() { + if (this.charSpacing !== 0) { + return this.fontSize * this.charSpacing / 1000; + } + return 0; + }, + + /** + * Retrieves the value of property at given character position + * @param {Number} lineIndex the line number + * @param {Number} charIndex the character number + * @param {String} property the property name + * @returns the value of 'property' + */ + getValueOfPropertyAt: function(lineIndex, charIndex, property) { + var charStyle = this._getStyleDeclaration(lineIndex, charIndex); + if (charStyle && typeof charStyle[property] !== 'undefined') { + return charStyle[property]; + } + return this[property]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextDecoration: function(ctx, type) { + if (!this[type] && !this.styleHas(type)) { + return; + } + var heightOfLine, size, _size, + lineLeftOffset, dy, _dy, + line, lastDecoration, + leftOffset = this._getLeftOffset(), + topOffset = this._getTopOffset(), top, + boxStart, boxWidth, charBox, currentDecoration, + maxHeight, currentFill, lastFill, path = this.path, + charSpacing = this._getWidthOfCharSpacing(), + offsetY = this.offsets[type]; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this[type] && !this.styleHas(type, i)) { + topOffset += heightOfLine; + continue; + } + line = this._textLines[i]; + maxHeight = heightOfLine / this.lineHeight; + lineLeftOffset = this._getLineLeftOffset(i); + boxStart = 0; + boxWidth = 0; + lastDecoration = this.getValueOfPropertyAt(i, 0, type); + lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); + top = topOffset + maxHeight * (1 - this._fontSizeFraction); + size = this.getHeightOfChar(i, 0); + dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentDecoration = this.getValueOfPropertyAt(i, j, type); + currentFill = this.getValueOfPropertyAt(i, j, 'fill'); + _size = this.getHeightOfChar(i, j); + _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); + if (path && currentDecoration && currentFill) { + ctx.save(); + ctx.fillStyle = lastFill; + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillRect( + -charBox.kernedWidth / 2, + offsetY * _size + _dy, + charBox.kernedWidth, + this.fontSize / 15 + ); + ctx.restore(); + } + else if ( + (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) + && boxWidth > 0 + ) { + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + if (lastDecoration && lastFill) { + ctx.fillStyle = lastFill; + ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth, + this.fontSize / 15 + ); + } + boxStart = charBox.left; + boxWidth = charBox.width; + lastDecoration = currentDecoration; + lastFill = currentFill; + size = _size; + dy = _dy; + } + else { + boxWidth += charBox.kernedWidth; + } + } + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentFill; + currentDecoration && currentFill && ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth - charSpacing, + this.fontSize / 15 + ); + topOffset += heightOfLine; + } + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * return font declaration string for canvas context + * @param {Object} [styleObject] object + * @returns {String} font declaration formatted for canvas context. + */ + _getFontDeclaration: function(styleObject, forMeasuring) { + var style = styleObject || this, family = this.fontFamily, + fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1; + var fontFamily = family === undefined || + family.indexOf('\'') > -1 || family.indexOf(',') > -1 || + family.indexOf('"') > -1 || fontIsGeneric + ? style.fontFamily : '"' + style.fontFamily + '"'; + return [ + // node-canvas needs "weight style", while browsers need "style weight" + // verify if this can be fixed in JSDOM + (fabric.isLikelyNode ? style.fontWeight : style.fontStyle), + (fabric.isLikelyNode ? style.fontStyle : style.fontWeight), + forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px', + fontFamily + ].join(' '); + }, + + /** + * Renders text instance on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if object is not visible + if (!this.visible) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + } + this.callSuper('render', ctx); + }, + + /** + * Returns the text as an array of lines. + * @param {String} text text to split + * @returns {Array} Lines in the text + */ + _splitTextIntoLines: function(text) { + var lines = text.split(this._reNewline), + newLines = new Array(lines.length), + newLine = ['\n'], + newText = []; + for (var i = 0; i < lines.length; i++) { + newLines[i] = fabric.util.string.graphemeSplit(lines[i]); + newText = newText.concat(newLines[i], newLine); + } + newText.pop(); + return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines }; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var allProperties = additionalProps.concat(propertiesToInclude); + var obj = this.callSuper('toObject', allProperties); + // styles will be overridden with a properly cloned structure + obj.styles = clone(this.styles, true); + if (obj.path) { + obj.path = this.path.toObject(); + } + return obj; + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + this.callSuper('set', key, value); + var needsDims = false; + var isAddingPath = false; + if (typeof key === 'object') { + for (var _key in key) { + if (_key === 'path') { + this.setPathInfo(); + } + needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1; + isAddingPath = isAddingPath || _key === 'path'; + } + } + else { + needsDims = this._dimensionAffectingProps.indexOf(key) !== -1; + isAddingPath = key === 'path'; + } + if (isAddingPath) { + this.setPathInfo(); + } + if (needsDims) { + this.initDimensions(); + this.setCoords(); + } + return this; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) + * @static + * @memberOf fabric.Text + * @see: http://www.w3.org/TR/SVG/text.html#TextElement + */ + fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( + 'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' ')); + + /** + * Default SVG font size + * @static + * @memberOf fabric.Text + */ + fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; + + /** + * Returns fabric.Text instance from an SVG element (not yet implemented) + * @static + * @memberOf fabric.Text + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Text.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES), + parsedAnchor = parsedAttributes.textAnchor || 'left'; + options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes); + + options.top = options.top || 0; + options.left = options.left || 0; + if (parsedAttributes.textDecoration) { + var textDecoration = parsedAttributes.textDecoration; + if (textDecoration.indexOf('underline') !== -1) { + options.underline = true; + } + if (textDecoration.indexOf('overline') !== -1) { + options.overline = true; + } + if (textDecoration.indexOf('line-through') !== -1) { + options.linethrough = true; + } + delete options.textDecoration; + } + if ('dx' in parsedAttributes) { + options.left += parsedAttributes.dx; + } + if ('dy' in parsedAttributes) { + options.top += parsedAttributes.dy; + } + if (!('fontSize' in options)) { + options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + + var textContent = ''; + + // The XML is not properly parsed in IE9 so a workaround to get + // textContent is through firstChild.data. Another workaround would be + // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) + if (!('textContent' in element)) { + if ('firstChild' in element && element.firstChild !== null) { + if ('data' in element.firstChild && element.firstChild.data !== null) { + textContent = element.firstChild.data; + } + } + } + else { + textContent = element.textContent; + } + + textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); + var originalStrokeWidth = options.strokeWidth; + options.strokeWidth = 0; + + var text = new fabric.Text(textContent, options), + textHeightScaleFactor = text.getScaledHeight() / text.height, + lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, + scaledDiff = lineHeightDiff * textHeightScaleFactor, + textHeight = text.getScaledHeight() + scaledDiff, + offX = 0; + /* + Adjust positioning: + x/y attributes in SVG correspond to the bottom-left corner of text bounding box + fabric output by default at top, left. + */ + if (parsedAnchor === 'center') { + offX = text.getScaledWidth() / 2; + } + if (parsedAnchor === 'right') { + offX = text.getScaledWidth(); + } + text.set({ + left: text.left - offX, + top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight, + strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1, + }); + callback(text); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Text instance from an object representation + * @static + * @memberOf fabric.Text + * @param {Object} object plain js Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created + */ + fabric.Text.fromObject = function(object, callback) { + var objectCopy = clone(object), path = object.path; + delete objectCopy.path; + return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { + if (path) { + fabric.Object._fromObject('Path', path, function(pathInstance) { + textInstance.set('path', pathInstance); + callback(textInstance); + }, 'path'); + } + else { + callback(textInstance); + } + }, 'text'); + }; + + fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text); + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return true; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Returns true if object has a style property or has it ina specified line + * This function is used to detect if a text will use a particular property or not. + * @param {String} property to check for + * @param {Number} lineIndex to check the style on + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (!this.styles || !property || property === '') { + return false; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return false; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] }; + // eslint-disable-next-line + for (var p1 in obj) { + // eslint-disable-next-line + for (var p2 in obj[p1]) { + if (typeof obj[p1][p2][property] !== 'undefined') { + return true; + } + } + } + return false; + }, + + /** + * Check if characters in a text have a value for a property + * whose value matches the textbox's value for that property. If so, + * the character-level property is deleted. If the character + * has no other properties, then it is also deleted. Finally, + * if the line containing that character has no other characters + * then it also is deleted. + * + * @param {string} property The property to compare between characters and text. + */ + cleanStyle: function(property) { + if (!this.styles || !property || property === '') { + return false; + } + var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue, + allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject; + // eslint-disable-next-line + for (var p1 in obj) { + letterCount = 0; + // eslint-disable-next-line + for (var p2 in obj[p1]) { + var styleObject = obj[p1][p2], + stylePropertyHasBeenSet = styleObject.hasOwnProperty(property); + + stylesCount++; + + if (stylePropertyHasBeenSet) { + if (!stylePropertyValue) { + stylePropertyValue = styleObject[property]; + } + else if (styleObject[property] !== stylePropertyValue) { + allStyleObjectPropertiesMatch = false; + } + + if (styleObject[property] === this[property]) { + delete styleObject[property]; + } + } + else { + allStyleObjectPropertiesMatch = false; + } + + if (Object.keys(styleObject).length !== 0) { + letterCount++; + } + else { + delete obj[p1][p2]; + } + } + + if (letterCount === 0) { + delete obj[p1]; + } + } + // if every grapheme has the same style set then + // delete those styles and set it on the parent + for (var i = 0; i < this._textLines.length; i++) { + graphemeCount += this._textLines[i].length; + } + if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) { + this[property] = stylePropertyValue; + this.removeStyle(property); + } + }, + + /** + * Remove a style property or properties from all individual character styles + * in a text object. Deletes the character style object if it contains no other style + * props. Deletes a line style object if it contains no other character styles. + * + * @param {String} props The property to remove from character styles. + */ + removeStyle: function(property) { + if (!this.styles || !property || property === '') { + return; + } + var obj = this.styles, line, lineNum, charNum; + for (lineNum in obj) { + line = obj[lineNum]; + for (charNum in line) { + delete line[charNum][property]; + if (Object.keys(line[charNum]).length === 0) { + delete line[charNum]; + } + } + if (Object.keys(line).length === 0) { + delete obj[lineNum]; + } + } + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this._getLineStyle(loc.lineIndex)) { + this._setLineStyle(loc.lineIndex); + } + + if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { + this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); + } + + fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. useful to manage styles. + */ + get2DCursorLocation: function(selectionStart, skipWrapping) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var lines = skipWrapping ? this._unwrappedTextLines : this._textLines, + len = lines.length; + for (var i = 0; i < len; i++) { + if (selectionStart <= lines[i].length) { + return { + lineIndex: i, + charIndex: selectionStart + }; + } + selectionStart -= lines[i].length + this.missingNewlineOffset(i); + } + return { + lineIndex: i - 1, + charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart + }; + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * if startIndex or endIndex are not provided, selectionStart or selectionEnd will be used. + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @param {Boolean} [complete] get full style or not + * @return {Array} styles an array with one, zero or more Style objects + */ + getSelectionStyles: function(startIndex, endIndex, complete) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + var styles = []; + for (var i = startIndex; i < endIndex; i++) { + styles.push(this.getStyleAtPosition(i, complete)); + } + return styles; + }, + + /** + * Gets style of a current selection/cursor position + * @param {Number} position to get styles at + * @param {Boolean} [complete] full style if true + * @return {Object} style Style object at a specified index + * @private + */ + getStyleAtPosition: function(position, complete) { + var loc = this.get2DCursorLocation(position), + style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : + this._getStyleDeclaration(loc.lineIndex, loc.charIndex); + return style || {}; + }, + + /** + * Sets style of a current selection, if no selection exist, do not set anything. + * @param {Object} [styles] Styles object + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles, startIndex, endIndex) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + for (var i = startIndex; i < endIndex; i++) { + this._extendStyles(i, styles); + } + /* not included in _extendStyles to avoid clearing cache more than once */ + this._forceClearCache = true; + return this; + }, + + /** + * get the reference, not a clone, of the style object for a given character + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Object} style object + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + var lineStyle = this.styles && this.styles[lineIndex]; + if (!lineStyle) { + return null; + } + return lineStyle[charIndex]; + }, + + /** + * return a new object that contains all the style property for a character + * the object returned is newly created + * @param {Number} lineIndex of the line where the character is + * @param {Number} charIndex position of the character on the line + * @return {Object} style object + */ + getCompleteStyleDeclaration: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex) || { }, + styleObject = { }, prop; + for (var i = 0; i < this._styleProperties.length; i++) { + prop = this._styleProperties[i]; + styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; + } + return styleObject; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + this.styles[lineIndex][charIndex] = style; + }, + + /** + * + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + delete this.styles[lineIndex][charIndex]; + }, + + /** + * @param {Number} lineIndex + * @return {Boolean} if the line exists or not + * @private + */ + _getLineStyle: function(lineIndex) { + return !!this.styles[lineIndex]; + }, + + /** + * Set the line style to an empty object so that is initialized + * @param {Number} lineIndex + * @private + */ + _setLineStyle: function(lineIndex) { + this.styles[lineIndex] = {}; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _deleteLineStyle: function(lineIndex) { + delete this.styles[lineIndex]; + } + }); +})(); + + +(function() { + + function parseDecoration(object) { + if (object.textDecoration) { + object.textDecoration.indexOf('underline') > -1 && (object.underline = true); + object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); + object.textDecoration.indexOf('overline') > -1 && (object.overline = true); + delete object.textDecoration; + } + } + + /** + * IText class (introduced in v1.4) Events are also fired with "text:" + * prefix when observing canvas. + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires changed + * @fires selection:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

    Supported key combinations:

    + *
    +   *   Move cursor:                    left, right, up, down
    +   *   Select character:               shift + left, shift + right
    +   *   Select text vertically:         shift + up, shift + down
    +   *   Move cursor by word:            alt + left, alt + right
    +   *   Select words:                   shift + alt + left, shift + alt + right
    +   *   Move cursor to line start/end:  cmd + left, cmd + right or home, end
    +   *   Select till start/end of line:  cmd + shift + left, cmd + shift + right or shift + home, shift + end
    +   *   Jump to start/end of text:      cmd + up, cmd + down
    +   *   Select till start/end of text:  cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
    +   *   Delete character:               backspace
    +   *   Delete word:                    alt + backspace
    +   *   Delete line:                    cmd + backspace
    +   *   Forward delete:                 delete
    +   *   Copy text:                      ctrl/cmd + c
    +   *   Paste text:                     ctrl/cmd + v
    +   *   Cut text:                       ctrl/cmd + x
    +   *   Select entire text:             ctrl/cmd + a
    +   *   Quit editing                    tab or esc
    +   * 
    + * + *

    Supported mouse/touch combination

    + *
    +   *   Position cursor:                click/touch
    +   *   Create selection:               click/touch & drag
    +   *   Create selection:               click & shift + click
    +   *   Select word:                    double click
    +   *   Select line:                    triple click
    +   * 
    + */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', + + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Number + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Number + * @default + */ + selectionEnd: 0, + + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', + + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, + + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, + + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of text cursor color in editing mode. + * if not set (default) will take color from the text. + * if set to a color value that fabric can understand, it will + * be used instead of the color of the text at the current position. + * @type String + * @default + */ + cursorColor: '', + + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Indicates whether internal text char widths can be cached + * @type Boolean + * @default + */ + caching: true, + + /** + * DOM container to append the hiddenTextarea. + * An alternative to attaching to the document.body. + * Useful to reduce laggish redraw of the full document.body tree and + * also with modals event capturing that won't let the textarea take focus. + * @type HTMLElement + * @default + */ + hiddenTextareaContainer: null, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + __widthOfSpace: [], + + /** + * Helps determining when the text is in composition, so that the cursor + * rendering is altered. + */ + inCompositionMode: false, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.callSuper('initialize', text, options); + this.initBehavior(); + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + index = Math.max(index, 0); + this._updateAndFire('selectionStart', index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + index = Math.min(index, this.text.length); + this._updateAndFire('selectionEnd', index); + }, + + /** + * @private + * @param {String} property 'selectionStart' or 'selectionEnd' + * @param {Number} index new position of property + */ + _updateAndFire: function(property, index) { + if (this[property] !== index) { + this._fireSelectionChanged(); + this[property] = index; + } + this._updateTextarea(); + }, + + /** + * Fires the even of selection changed + * @private + */ + _fireSelectionChanged: function() { + this.fire('selection:changed'); + this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + }, + + /** + * Initialize text dimensions. Render all text on given context + * or on a offscreen canvas to get the text width with measureText. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + * @private + */ + initDimensions: function() { + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this.callSuper('initDimensions'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + this.clearContextTop(); + this.callSuper('render', ctx); + // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor + // the correct position but not at every cursor animation. + this.cursorOffsetCache = { }; + this.renderCursorOrSelection(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + }, + + /** + * Prepare and clean the contextTop + */ + clearContextTop: function(skipRestore) { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { + return; + } + var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this.transform(ctx); + this._clearTextArea(ctx); + skipRestore || ctx.restore(); + }, + /** + * Renders cursor or selection (depending on what exists) + * it does on the contextTop. If contextTop is not available, do nothing. + */ + renderCursorOrSelection: function() { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { + return; + } + var boundaries = this._getCursorBoundaries(), + ctx = this.canvas.contextTop; + this.clearContextTop(true); + if (this.selectionStart === this.selectionEnd) { + this.renderCursor(boundaries, ctx); + } + else { + this.renderSelection(boundaries, ctx); + } + ctx.restore(); + }, + + _clearTextArea: function(ctx) { + // we add 4 pixel, to be sure to do not leave any pixel out + var width = this.width + 4, height = this.height + 4; + ctx.clearRect(-width / 2, -height / 2, width, height); + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(position) { + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + if (typeof position === 'undefined') { + position = this.selectionStart; + } + + var left = this._getLeftOffset(), + top = this._getTopOffset(), + offsets = this._getCursorBoundariesOffsets(position); + return { + left: left, + top: top, + leftOffset: offsets.left, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(position) { + if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { + return this.cursorOffsetCache; + } + var lineLeftOffset, + lineIndex, + charIndex, + topOffset = 0, + leftOffset = 0, + boundaries, + cursorPosition = this.get2DCursorLocation(position); + charIndex = cursorPosition.charIndex; + lineIndex = cursorPosition.lineIndex; + for (var i = 0; i < lineIndex; i++) { + topOffset += this.getHeightOfLine(i); + } + lineLeftOffset = this._getLineLeftOffset(lineIndex); + var bound = this.__charBounds[lineIndex][charIndex]; + bound && (leftOffset = bound.left); + if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) { + leftOffset -= this._getWidthOfCharSpacing(); + } + boundaries = { + top: topOffset, + left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), + }; + if (this.direction === 'rtl') { + boundaries.left *= -1; + } + this.cursorOffsetCache = boundaries; + return this.cursorOffsetCache; + }, + + /** + * Renders cursor + * @param {Object} boundaries + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderCursor: function(boundaries, ctx) { + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'), + multiplier = this.scaleX * this.canvas.getZoom(), + cursorWidth = this.cursorWidth / multiplier, + topOffset = boundaries.topOffset, + dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY'); + topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight + - charHeight * (1 - this._fontSizeFraction); + + if (this.inCompositionMode) { + this.renderSelection(boundaries, ctx); + } + ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill'); + ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; + ctx.fillRect( + boundaries.left + boundaries.leftOffset - cursorWidth / 2, + topOffset + boundaries.top + dy, + cursorWidth, + charHeight); + }, + + /** + * Renders text selection + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderSelection: function(boundaries, ctx) { + + var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart, + selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd, + isJustify = this.textAlign.indexOf('justify') !== -1, + start = this.get2DCursorLocation(selectionStart), + end = this.get2DCursorLocation(selectionEnd), + startLine = start.lineIndex, + endLine = end.lineIndex, + startChar = start.charIndex < 0 ? 0 : start.charIndex, + endChar = end.charIndex < 0 ? 0 : end.charIndex; + + for (var i = startLine; i <= endLine; i++) { + var lineOffset = this._getLineLeftOffset(i) || 0, + lineHeight = this.getHeightOfLine(i), + realLineHeight = 0, boxStart = 0, boxEnd = 0; + + if (i === startLine) { + boxStart = this.__charBounds[startLine][startChar].left; + } + if (i >= startLine && i < endLine) { + boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5? + } + else if (i === endLine) { + if (endChar === 0) { + boxEnd = this.__charBounds[endLine][endChar].left; + } + else { + var charSpacing = this._getWidthOfCharSpacing(); + boxEnd = this.__charBounds[endLine][endChar - 1].left + + this.__charBounds[endLine][endChar - 1].width - charSpacing; + } + } + realLineHeight = lineHeight; + if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) { + lineHeight /= this.lineHeight; + } + var drawStart = boundaries.left + lineOffset + boxStart, + drawWidth = boxEnd - boxStart, + drawHeight = lineHeight, extraTop = 0; + if (this.inCompositionMode) { + ctx.fillStyle = this.compositionColor || 'black'; + drawHeight = 1; + extraTop = lineHeight; + } + else { + ctx.fillStyle = this.selectionColor; + } + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - drawWidth; + } + ctx.fillRect( + drawStart, + boundaries.top + boundaries.topOffset + extraTop, + drawWidth, + drawHeight); + boundaries.topOffset += realLineHeight; + } + }, + + /** + * High level function to know the height of the cursor. + * the currentChar is the one that precedes the cursor + * Returns fontSize of char at the current cursor + * Unused from the library, is for the end user + * @return {Number} Character font size + */ + getCurrentCharFontSize: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize'); + }, + + /** + * High level function to know the color of the cursor. + * the currentChar is the one that precedes the cursor + * Returns color (fill) of char at the current cursor + * if the text object has a pattern or gradient for filler, it will return that. + * Unused by the library, is for the end user + * @return {String | fabric.Gradient | fabric.Pattern} Character color (fill) + */ + getCurrentCharColor: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fill'); + }, + + /** + * Returns the cursor position for the getCurrent.. functions + * @private + */ + _getCurrentCharIndex: function() { + var cursorPosition = this.get2DCursorLocation(this.selectionStart, true), + charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0; + return { l: cursorPosition.lineIndex, c: charIndex }; + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as argument + */ + fabric.IText.fromObject = function(object, callback) { + parseDecoration(object); + if (object.styles) { + for (var i in object.styles) { + for (var j in object.styles[i]) { + parseDecoration(object.styles[i][j]); + } + } + } + fabric.Object._fromObject('IText', object, callback, 'text'); + }; +})(); + + +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initAddedHandler(); + this.initRemovedHandler(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.mouseMoveHandler = this.mouseMoveHandler.bind(this); + }, + + onDeselect: function() { + this.isEditing && this.exitEditing(); + this.selected = false; + }, + + /** + * Initializes "added" event handler + */ + initAddedHandler: function() { + var _this = this; + this.on('added', function() { + var canvas = _this.canvas; + if (canvas) { + if (!canvas._hasITextHandlers) { + canvas._hasITextHandlers = true; + _this._initCanvasHandlers(canvas); + } + canvas._iTextInstances = canvas._iTextInstances || []; + canvas._iTextInstances.push(_this); + } + }); + }, + + initRemovedHandler: function() { + var _this = this; + this.on('removed', function() { + var canvas = _this.canvas; + if (canvas) { + canvas._iTextInstances = canvas._iTextInstances || []; + fabric.util.removeFromArray(canvas._iTextInstances, _this); + if (canvas._iTextInstances.length === 0) { + canvas._hasITextHandlers = false; + _this._removeCanvasHandlers(canvas); + } + } + }); + }, + + /** + * register canvas event to manage exiting on other instances + * @private + */ + _initCanvasHandlers: function(canvas) { + canvas._mouseUpITextHandler = function() { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.__isMousedown = false; + }); + } + }; + canvas.on('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * remove canvas event to manage exiting on other instances + * @private + */ + _removeCanvasHandlers: function(canvas) { + canvas.off('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * @private + */ + _tick: function() { + this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete'); + }, + + /** + * @private + */ + _animateCursor: function(obj, targetOpacity, duration, completeMethod) { + + var tickState; + + tickState = { + isAborted: false, + abort: function() { + this.isAborted = true; + }, + }; + + obj.animate('_currentCursorOpacity', targetOpacity, { + duration: duration, + onComplete: function() { + if (!tickState.isAborted) { + obj[completeMethod](); + } + }, + onChange: function() { + // we do not want to animate a selection, only cursor + if (obj.canvas && obj.selectionStart === obj.selectionEnd) { + obj.renderCursorOrSelection(); + } + }, + abort: function() { + return tickState.isAborted; + } + }); + return tickState; + }, + + /** + * @private + */ + _onTickComplete: function() { + + var _this = this; + + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick'); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function(restart) { + var _this = this, + delay = restart ? 0 : this.cursorDelay; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this._cursorTimeout2 = setTimeout(function() { + _this._tick(); + }, delay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + var shouldClear = this._currentTickState || this._currentTickCompleteState, + canvas = this.canvas; + this._currentTickState && this._currentTickState.abort(); + this._currentTickCompleteState && this._currentTickCompleteState.abort(); + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + // to clear just itext area we need to transform the context + // it may not be worth it + if (shouldClear && canvas) { + canvas.clearContext(canvas.contextTop || canvas.contextContainer); + } + + }, + + /** + * Selects entire text + * @return {fabric.IText} thisArg + * @chainable + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this._text.length; + this._fireSelectionChanged(); + this._updateTextarea(); + return this; + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this._text.slice(this.selectionStart, this.selectionEnd).join(''); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { + offset++; + index--; + } + } + while (/\S/.test(this._text[index]) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { + offset++; + index++; + } + } + while (/\S/.test(this._text[index]) && index < this._text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this._text[index]) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this._text[index]) && index < this._text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction 1 or -1 + * @return {Number} Index of the beginning or end of a word + */ + searchWordBoundary: function(selectionStart, direction) { + var text = this._text, + index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart, + _char = text[index], + // wrong + reNonWord = fabric.reNonWord; + + while (!reNonWord.test(_char) && index > 0 && index < text.length) { + index += direction; + _char = text[index]; + } + if (reNonWord.test(_char)) { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */ + newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + * @return {fabric.IText} thisArg + * @chainable + */ + selectLine: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.findLineBoundaryLeft(selectionStart), + newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + return this; + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function(e) { + if (this.isEditing || !this.editable) { + return; + } + + if (this.canvas) { + this.canvas.calcOffset(); + this.exitEditingOnOthers(this.canvas); + } + + this.isEditing = true; + + this.initHiddenTextarea(e); + this.hiddenTextarea.focus(); + this.hiddenTextarea.value = this.text; + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + this._textBeforeEdit = this.text; + + this._tick(); + this.fire('editing:entered'); + this._fireSelectionChanged(); + if (!this.canvas) { + return this; + } + this.canvas.fire('text:editing:entered', { target: this }); + this.initMouseMoveHandler(); + this.canvas.requestRenderAll(); + return this; + }, + + exitEditingOnOthers: function(canvas) { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.selected = false; + if (obj.isEditing) { + obj.exitEditing(); + } + }); + } + }, + + /** + * Initializes "mousemove" event handler + */ + initMouseMoveHandler: function() { + this.canvas.on('mouse:move', this.mouseMoveHandler); + }, + + /** + * @private + */ + mouseMoveHandler: function(options) { + if (!this.__isMousedown || !this.isEditing) { + return; + } + + var newSelectionStart = this.getSelectionStartFromPointer(options.e), + currentStart = this.selectionStart, + currentEnd = this.selectionEnd; + if ( + (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) + && + (currentStart === newSelectionStart || currentEnd === newSelectionStart) + ) { + return; + } + if (newSelectionStart > this.__selectionStartOnMouseDown) { + this.selectionStart = this.__selectionStartOnMouseDown; + this.selectionEnd = newSelectionStart; + } + else { + this.selectionStart = newSelectionStart; + this.selectionEnd = this.__selectionStartOnMouseDown; + } + if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { + this.restartCursorIfNeeded(); + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + } + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * convert from textarea to grapheme indexes + */ + fromStringToGraphemeSelection: function(start, end, text) { + var smallerTextStart = text.slice(0, start), + graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; + if (start === end) { + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + } + var smallerTextEnd = text.slice(start, end), + graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + }, + + /** + * convert from fabric to textarea values + */ + fromGraphemeToStringSelection: function(start, end, _text) { + var smallerTextStart = _text.slice(0, start), + graphemeStart = smallerTextStart.join('').length; + if (start === end) { + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + } + var smallerTextEnd = _text.slice(start, end), + graphemeEnd = smallerTextEnd.join('').length; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + }, + + /** + * @private + */ + _updateTextarea: function() { + this.cursorOffsetCache = { }; + if (!this.hiddenTextarea) { + return; + } + if (!this.inCompositionMode) { + var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text); + this.hiddenTextarea.selectionStart = newSelection.selectionStart; + this.hiddenTextarea.selectionEnd = newSelection.selectionEnd; + } + this.updateTextareaPosition(); + }, + + /** + * @private + */ + updateFromTextArea: function() { + if (!this.hiddenTextarea) { + return; + } + this.cursorOffsetCache = { }; + this.text = this.hiddenTextarea.value; + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + var newSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value); + this.selectionEnd = this.selectionStart = newSelection.selectionEnd; + if (!this.inCompositionMode) { + this.selectionStart = newSelection.selectionStart; + } + this.updateTextareaPosition(); + }, + + /** + * @private + */ + updateTextareaPosition: function() { + if (this.selectionStart === this.selectionEnd) { + var style = this._calcTextareaPosition(); + this.hiddenTextarea.style.left = style.left; + this.hiddenTextarea.style.top = style.top; + } + }, + + /** + * @private + * @return {Object} style contains style for hiddenTextarea + */ + _calcTextareaPosition: function() { + if (!this.canvas) { + return { x: 1, y: 1 }; + } + var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart, + boundaries = this._getCursorBoundaries(desiredPosition), + cursorLocation = this.get2DCursorLocation(desiredPosition), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight, + leftOffset = boundaries.leftOffset, + m = this.calcTransformMatrix(), + p = { + x: boundaries.left + leftOffset, + y: boundaries.top + boundaries.topOffset + charHeight + }, + retinaScaling = this.canvas.getRetinaScaling(), + upperCanvas = this.canvas.upperCanvasEl, + upperCanvasWidth = upperCanvas.width / retinaScaling, + upperCanvasHeight = upperCanvas.height / retinaScaling, + maxWidth = upperCanvasWidth - charHeight, + maxHeight = upperCanvasHeight - charHeight, + scaleX = upperCanvas.clientWidth / upperCanvasWidth, + scaleY = upperCanvas.clientHeight / upperCanvasHeight; + + p = fabric.util.transformPoint(p, m); + p = fabric.util.transformPoint(p, this.canvas.viewportTransform); + p.x *= scaleX; + p.y *= scaleY; + if (p.x < 0) { + p.x = 0; + } + if (p.x > maxWidth) { + p.x = maxWidth; + } + if (p.y < 0) { + p.y = 0; + } + if (p.y > maxHeight) { + p.y = maxHeight; + } + + // add canvas offset on document + p.x += this.canvas._offset.left; + p.y += this.canvas._offset.top; + + return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight }; + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + selectable: this.selectable, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) { + return; + } + + this.hoverCursor = this._savedProps.hoverCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.selectable = this._savedProps.selectable; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + var isTextChanged = (this._textBeforeEdit !== this.text); + var hiddenTextarea = this.hiddenTextarea; + this.selected = false; + this.isEditing = false; + + this.selectionEnd = this.selectionStart; + + if (hiddenTextarea) { + hiddenTextarea.blur && hiddenTextarea.blur(); + hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea); + } + this.hiddenTextarea = null; + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this.fire('editing:exited'); + isTextChanged && this.fire('modified'); + if (this.canvas) { + this.canvas.off('mouse:move', this.mouseMoveHandler); + this.canvas.fire('text:editing:exited', { target: this }); + isTextChanged && this.canvas.fire('object:modified', { target: this }); + } + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + for (var prop in this.styles) { + if (!this._textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * remove and reflow a style block from start to end. + * @param {Number} start linear start position for removal (included in removal) + * @param {Number} end linear end position for removal ( excluded from removal ) + */ + removeStyleFromTo: function(start, end) { + var cursorStart = this.get2DCursorLocation(start, true), + cursorEnd = this.get2DCursorLocation(end, true), + lineStart = cursorStart.lineIndex, + charStart = cursorStart.charIndex, + lineEnd = cursorEnd.lineIndex, + charEnd = cursorEnd.charIndex, + i, styleObj; + if (lineStart !== lineEnd) { + // step1 remove the trailing of lineStart + if (this.styles[lineStart]) { + for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) { + delete this.styles[lineStart][i]; + } + } + // step2 move the trailing of lineEnd to lineStart if needed + if (this.styles[lineEnd]) { + for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) { + styleObj = this.styles[lineEnd][i]; + if (styleObj) { + this.styles[lineStart] || (this.styles[lineStart] = { }); + this.styles[lineStart][charStart + i - charEnd] = styleObj; + } + } + } + // step3 detects lines will be completely removed. + for (i = lineStart + 1; i <= lineEnd; i++) { + delete this.styles[i]; + } + // step4 shift remaining lines. + this.shiftLineStyles(lineEnd, lineStart - lineEnd); + } + else { + // remove and shift left on the same line + if (this.styles[lineStart]) { + styleObj = this.styles[lineStart]; + var diff = charEnd - charStart, numericChar, _char; + for (i = charStart; i < charEnd; i++) { + delete styleObj[i]; + } + for (_char in this.styles[lineStart]) { + numericChar = parseInt(_char, 10); + if (numericChar >= charEnd) { + styleObj[numericChar - diff] = styleObj[_char]; + delete styleObj[_char]; + } + } + } + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can any number? + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by offset upward or downward + // do not clone deep. we need new array, not new style objects + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + if (!clonedStyles[numericLine - offset]) { + delete this.styles[numericLine]; + } + } + } + }, + + restartCursorIfNeeded: function() { + if (!this._currentTickState || this._currentTickState.isAborted + || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted + ) { + this.initDelayedCursor(); + } + }, + + /** + * Handle insertion of more consecutive style lines for when one or more + * newlines gets added to the text. Since current style needs to be shifted + * first we shift the current style of the number lines needed, then we add + * new lines from the last to the first. + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Number} qty number of lines to add + * @param {Array} copiedStyle Array of objects styles + */ + insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) { + var currentCharStyle, + newLineStyles = {}, + somethingAdded = false, + isEndOfLine = this._unwrappedTextLines[lineIndex].length === charIndex; + + qty || (qty = 1); + this.shiftLineStyles(lineIndex, qty); + if (this.styles[lineIndex]) { + currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1]; + } + // we clone styles of all chars + // after cursor onto the current line + for (var index in this.styles[lineIndex]) { + var numIndex = parseInt(index, 10); + if (numIndex >= charIndex) { + somethingAdded = true; + newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + if (!(isEndOfLine && charIndex === 0)) { + delete this.styles[lineIndex][index]; + } + } + } + var styleCarriedOver = false; + if (somethingAdded && !isEndOfLine) { + // if is end of line, the extra style we copied + // is probably not something we want + this.styles[lineIndex + qty] = newLineStyles; + styleCarriedOver = true; + } + if (styleCarriedOver) { + // skip the last line of since we already prepared it. + qty--; + } + // for the all the lines or all the other lines + // we clone current char style onto the next (otherwise empty) line + while (qty > 0) { + if (copiedStyle && copiedStyle[qty - 1]) { + this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty - 1]) }; + } + else if (currentCharStyle) { + this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) }; + } + else { + delete this.styles[lineIndex + qty]; + } + qty--; + } + this._forceClearCache = true; + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Number} quantity number Style object to insert, if given + * @param {Array} copiedStyle array of style objects + */ + insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) { + if (!this.styles) { + this.styles = {}; + } + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {}; + + quantity || (quantity = 1); + // shift all char styles by quantity forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex]; + // only delete the style if there was nothing moved there + if (!currentLineStylesCloned[numericIndex - quantity]) { + delete currentLineStyles[numericIndex]; + } + } + } + this._forceClearCache = true; + if (copiedStyle) { + while (quantity--) { + if (!Object.keys(copiedStyle[quantity]).length) { + continue; + } + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = {}; + } + this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]); + } + return; + } + if (!currentLineStyles) { + return; + } + var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1]; + while (newStyle && quantity--) { + this.styles[lineIndex][charIndex + quantity] = clone(newStyle); + } + }, + + /** + * Inserts style object(s) + * @param {Array} insertedText Characters at the location where style is inserted + * @param {Number} start cursor index for inserting style + * @param {Array} [copiedStyle] array of style objects to insert. + */ + insertNewStyleBlock: function(insertedText, start, copiedStyle) { + var cursorLoc = this.get2DCursorLocation(start, true), + addedLines = [0], linesLength = 0; + // get an array of how many char per lines are being added. + for (var i = 0; i < insertedText.length; i++) { + if (insertedText[i] === '\n') { + linesLength++; + addedLines[linesLength] = 0; + } + else { + addedLines[linesLength]++; + } + } + // for the first line copy the style from the current char position. + if (addedLines[0] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle); + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1); + } + linesLength && this.insertNewlineStyleObject( + cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength); + for (var i = 1; i < linesLength; i++) { + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); + } + else if (copiedStyle) { + this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0]; + } + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1); + } + // we use i outside the loop to get it like linesLength + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); + } + }, + + /** + * Set the selectionStart and selectionEnd according to the new position of cursor + * mimic the key - mouse navigation when shift is pressed. + */ + setSelectionStartEndWithShift: function(start, end, newSelection) { + if (newSelection <= start) { + if (end === start) { + this._selectionDirection = 'left'; + } + else if (this._selectionDirection === 'right') { + this._selectionDirection = 'left'; + this.selectionEnd = start; + } + this.selectionStart = newSelection; + } + else if (newSelection > start && newSelection < end) { + if (this._selectionDirection === 'right') { + this.selectionEnd = newSelection; + } + else { + this.selectionStart = newSelection; + } + } + else { + // newSelection is > selection start and end + if (end === start) { + this._selectionDirection = 'right'; + } + else if (this._selectionDirection === 'left') { + this._selectionDirection = 'right'; + this.selectionStart = end; + } + this.selectionEnd = newSelection; + } + }, + + setSelectionInBoundaries: function() { + var length = this.text.length; + if (this.selectionStart > length) { + this.selectionStart = length; + } + else if (this.selectionStart < 0) { + this.selectionStart = 0; + } + if (this.selectionEnd > length) { + this.selectionEnd = length; + } + else if (this.selectionEnd < 0) { + this.selectionEnd = 0; + } + } + }); +})(); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.__lastPointer = { }; + + this.on('mousedown', this.onMouseDown); + }, + + /** + * Default event handler to simulate triple click + * @private + */ + onMouseDown: function(options) { + if (!this.canvas) { + return; + } + this.__newClickTime = +new Date(); + var newPointer = options.pointer; + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + this.__lastIsEditing = this.isEditing; + this.__lastSelected = this.selected; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + /** + * @private + */ + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, + + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initMousedownHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, + + /** + * Default handler for double click, select a word + */ + doubleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Default handler for triple click, select a line + */ + tripleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('mousedblclick', this.doubleClickHandler); + this.on('tripleclick', this.tripleClickHandler); + }, + + /** + * Default event handler for the basic functionalities needed on _mouseDown + * can be overridden to do something different. + * Scope of this implementation is: find the click position, set selectionStart + * find selectionEnd, initialize the drawing of either cursor or selection area + * initializing a mousedDown on a text area will cancel fabricjs knowledge of + * current compositionMode. It will be set to false. + */ + _mouseDownHandler: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + + this.__isMousedown = true; + + if (this.selected) { + this.inCompositionMode = false; + this.setCursorByClick(options.e); + } + + if (this.isEditing) { + this.__selectionStartOnMouseDown = this.selectionStart; + if (this.selectionStart === this.selectionEnd) { + this.abortCursorAnimation(); + } + this.renderCursorOrSelection(); + } + }, + + /** + * Default event handler for the basic functionalities needed on mousedown:before + * can be overridden to do something different. + * Scope of this implementation is: verify the object is already selected when mousing down + */ + _mouseDownHandlerBefore: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + // we want to avoid that an object that was selected and then becomes unselectable, + // may trigger editing mode in some way. + this.selected = this === this.canvas._activeObject; + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', this._mouseDownHandler); + this.on('mousedown:before', this._mouseDownHandlerBefore); + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', this.mouseUpHandler); + }, + + /** + * standard handler for mouse up, overridable + * @private + */ + mouseUpHandler: function(options) { + this.__isMousedown = false; + if (!this.editable || this.group || + (options.transform && options.transform.actionPerformed) || + (options.e.button && options.e.button !== 1)) { + return; + } + + if (this.canvas) { + var currentActive = this.canvas._activeObject; + if (currentActive && currentActive !== this) { + // avoid running this logic when there is an active object + // this because is possible with shift click and fast clicks, + // to rapidly deselect and reselect this object and trigger an enterEdit + return; + } + } + + if (this.__lastSelected && !this.__corner) { + this.selected = false; + this.__lastSelected = false; + this.enterEditing(options.e); + if (this.selectionStart === this.selectionEnd) { + this.initDelayedCursor(true); + } + else { + this.renderCursorOrSelection(); + } + } + else { + this.selected = true; + } + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Event} e Event object + */ + setCursorByClick: function(e) { + var newSelection = this.getSelectionStartFromPointer(e), + start = this.selectionStart, end = this.selectionEnd; + if (e.shiftKey) { + this.setSelectionStartEndWithShift(start, end, newSelection); + } + else { + this.selectionStart = newSelection; + this.selectionEnd = newSelection; + } + if (this.isEditing) { + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + var mouseOffset = this.getLocalPointer(e), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + lineIndex = 0, + lineLeftOffset, + line; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (height <= mouseOffset.y) { + height += this.getHeightOfLine(i) * this.scaleY; + lineIndex = i; + if (i > 0) { + charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1); + } + } + else { + break; + } + } + lineLeftOffset = this._getLineLeftOffset(lineIndex); + width = lineLeftOffset * this.scaleX; + line = this._textLines[lineIndex]; + // handling of RTL: in order to get things work correctly, + // we assume RTL writing is mirrored compared to LTR writing. + // so in position detection we mirror the X offset, and when is time + // of rendering it, we mirror it again. + if (this.direction === 'rtl') { + mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + } + for (var j = 0, jlen = line.length; j < jlen; j++) { + prevWidth = width; + // i removed something about flipX here, check. + width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX; + if (width <= mouseOffset.x) { + charIndex++; + } + else { + break; + } + } + return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen); + }, + + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0 + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor || + distanceBtwNextCharAndCursor < 0 ? 0 : 1, + newSelectionStart = index + offset; + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this._text.length) { + newSelectionStart = this._text.length; + } + + return newSelectionStart; + } +}); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.setAttribute('autocorrect', 'off'); + this.hiddenTextarea.setAttribute('autocomplete', 'off'); + this.hiddenTextarea.setAttribute('spellcheck', 'false'); + this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', ''); + this.hiddenTextarea.setAttribute('wrap', 'off'); + var style = this._calcTextareaPosition(); + // line-height: 1px; was removed from the style to fix this: + // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 + this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + + ' paddingーtop: ' + style.fontSize + ';'; + + if (this.hiddenTextareaContainer) { + this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); + } + else { + fabric.document.body.appendChild(this.hiddenTextarea); + } + + fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); + + if (!this._clickHandlerInitialized && this.canvas) { + fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this)); + this._clickHandlerInitialized = true; + } + }, + + /** + * For functionalities on keyDown + * Map a special key to a function of the instance/prototype + * If you need different behaviour for ESC or TAB or arrows, you have to change + * this map setting the name of a function that you build on the fabric.Itext or + * your prototype. + * the map change will affect all Instances unless you need for only some text Instances + * in that case you have to clone this object and assign your Instance. + * this.keysMap = fabric.util.object.clone(this.keysMap); + * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0] + */ + keysMap: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorRight', + 36: 'moveCursorLeft', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + }, + + keysMapRtl: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorLeft', + 36: 'moveCursorRight', + 37: 'moveCursorRight', + 38: 'moveCursorUp', + 39: 'moveCursorLeft', + 40: 'moveCursorDown', + }, + + /** + * For functionalities on keyUp + ctrl || cmd + */ + ctrlKeysMapUp: { + 67: 'copy', + 88: 'cut' + }, + + /** + * For functionalities on keyDown + ctrl || cmd + */ + ctrlKeysMapDown: { + 65: 'selectAll' + }, + + onClick: function() { + // No need to trigger click event here, focus is enough to have the keyboard appear on Android + this.hiddenTextarea && this.hiddenTextarea.focus(); + }, + + /** + * Handles keydown event + * only used for arrows and combination of modifier keys. + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) { + return; + } + var keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap; + if (e.keyCode in keyMap) { + this[keyMap[e.keyCode]](e); + } + else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapDown[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.keyCode >= 33 && e.keyCode <= 40) { + // if i press an arrow key just update selection + this.inCompositionMode = false; + this.clearContextTop(); + this.renderCursorOrSelection(); + } + else { + this.canvas && this.canvas.requestRenderAll(); + } + }, + + /** + * Handles keyup event + * We handle KeyUp because ie11 and edge have difficulties copy/pasting + * if a copy/cut event fired, keyup is dismissed + * @param {Event} e Event object + */ + onKeyUp: function(e) { + if (!this.isEditing || this._copyDone || this.inCompositionMode) { + this._copyDone = false; + return; + } + if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapUp[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + this.canvas && this.canvas.requestRenderAll(); + }, + + /** + * Handles onInput event + * @param {Event} e Event object + */ + onInput: function(e) { + var fromPaste = this.fromPaste; + this.fromPaste = false; + e && e.stopPropagation(); + if (!this.isEditing) { + return; + } + // decisions about style changes. + var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText, + charCount = this._text.length, + nextCharCount = nextText.length, + removedText, insertedText, + charDiff = nextCharCount - charCount, + selectionStart = this.selectionStart, selectionEnd = this.selectionEnd, + selection = selectionStart !== selectionEnd, + copiedStyle, removeFrom, removeTo; + if (this.hiddenTextarea.value === '') { + this.styles = { }; + this.updateFromTextArea(); + this.fire('changed'); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.requestRenderAll(); + } + return; + } + + var textareaSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, + this.hiddenTextarea.selectionEnd, + this.hiddenTextarea.value + ); + var backDelete = selectionStart > textareaSelection.selectionStart; + + if (selection) { + removedText = this._text.slice(selectionStart, selectionEnd); + charDiff += selectionEnd - selectionStart; + } + else if (nextCharCount < charCount) { + if (backDelete) { + removedText = this._text.slice(selectionEnd + charDiff, selectionEnd); + } + else { + removedText = this._text.slice(selectionStart, selectionStart - charDiff); + } + } + insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd); + if (removedText && removedText.length) { + if (insertedText.length) { + // let's copy some style before deleting. + // we want to copy the style before the cursor OR the style at the cursor if selection + // is bigger than 0. + copiedStyle = this.getSelectionStyles(selectionStart, selectionStart + 1, false); + // now duplicate the style one for each inserted text. + copiedStyle = insertedText.map(function() { + // this return an array of references, but that is fine since we are + // copying the style later. + return copiedStyle[0]; + }); + } + if (selection) { + removeFrom = selectionStart; + removeTo = selectionEnd; + } + else if (backDelete) { + // detect differences between forwardDelete and backDelete + removeFrom = selectionEnd - removedText.length; + removeTo = selectionEnd; + } + else { + removeFrom = selectionEnd; + removeTo = selectionEnd + removedText.length; + } + this.removeStyleFromTo(removeFrom, removeTo); + } + if (insertedText.length) { + if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) { + copiedStyle = fabric.copiedTextStyle; + } + this.insertNewStyleBlock(insertedText, selectionStart, copiedStyle); + } + this.updateFromTextArea(); + this.fire('changed'); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.requestRenderAll(); + } + }, + /** + * Composition start + */ + onCompositionStart: function() { + this.inCompositionMode = true; + }, + + /** + * Composition end + */ + onCompositionEnd: function() { + this.inCompositionMode = false; + }, + + // /** + // * Composition update + // */ + onCompositionUpdate: function(e) { + this.compositionStart = e.target.selectionStart; + this.compositionEnd = e.target.selectionEnd; + this.updateTextareaPosition(); + }, + + /** + * Copies selected text + * @param {Event} e Event object + */ + copy: function() { + if (this.selectionStart === this.selectionEnd) { + //do not cut-copy if no selection + return; + } + + fabric.copiedText = this.getSelectedText(); + if (!fabric.disableStyleCopyPaste) { + fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true); + } + else { + fabric.copiedTextStyle = null; + } + this._copyDone = true; + }, + + /** + * Pastes text + * @param {Event} e Event object + */ + paste: function() { + this.fromPaste = true; + }, + + /** + * @private + * @param {Event} e Event object + * @return {Object} Clipboard data object + */ + _getClipboardData: function(e) { + return (e && e.clipboardData) || fabric.window.clipboardData; + }, + + /** + * Finds the width in pixels before the cursor on the same line + * @private + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Number} widthBeforeCursor width before cursor + */ + _getWidthBeforeCursor: function(lineIndex, charIndex) { + var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound; + + if (charIndex > 0) { + bound = this.__charBounds[lineIndex][charIndex - 1]; + widthBeforeCursor += bound.left + bound.width; + } + return widthBeforeCursor; + }, + + /** + * Gets start offset of a selection + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + // if on last line, down cursor goes to end of line + if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) { + // move to the end of a text + return this._text.length - selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor), + textAfterCursor = this._textLines[lineIndex].slice(charIndex); + return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex); + }, + + /** + * private + * Helps finding if the offset should be counted from Start or End + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + _getSelectionForOffset: function(e, isRight) { + if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) { + return this.selectionEnd; + } + else { + return this.selectionStart; + } + }, + + /** + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getUpCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + if (lineIndex === 0 || e.metaKey || e.keyCode === 33) { + // if on first line, up cursor goes to start of line + return -selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), + textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), + missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1); + // return a negative offset + return -this._textLines[lineIndex - 1].length + + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset); + }, + + /** + * for a given width it founds the matching character. + * @private + */ + _getIndexOnLine: function(lineIndex, width) { + + var line = this._textLines[lineIndex], + lineLeftOffset = this._getLineLeftOffset(lineIndex), + widthOfCharsOnLine = lineLeftOffset, + indexOnLine = 0, charWidth, foundMatch; + + for (var j = 0, jlen = line.length; j < jlen; j++) { + charWidth = this.__charBounds[lineIndex][j].width; + widthOfCharsOnLine += charWidth; + if (widthOfCharsOnLine > width) { + foundMatch = true; + var leftEdge = widthOfCharsOnLine - charWidth, + rightEdge = widthOfCharsOnLine, + offsetFromLeftEdge = Math.abs(leftEdge - width), + offsetFromRightEdge = Math.abs(rightEdge - width); + + indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + break; + } + } + + // reached end + if (!foundMatch) { + indexOnLine = line.length - 1; + } + + return indexOnLine; + }, + + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { + return; + } + this._moveCursorUpOrDown('Down', e); + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorUpOrDown('Up', e); + }, + + /** + * Moves cursor up or down, fires the events + * @param {String} direction 'Up' or 'Down' + * @param {Event} e Event object + */ + _moveCursorUpOrDown: function(direction, e) { + // getUpCursorOffset + // getDownCursorOffset + var action = 'get' + direction + 'CursorOffset', + offset = this[action](e, this._selectionDirection === 'right'); + if (e.shiftKey) { + this.moveCursorWithShift(offset); + } + else { + this.moveCursorWithoutShift(offset); + } + if (offset !== 0) { + this.setSelectionInBoundaries(); + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor with shift + * @param {Number} offset + */ + moveCursorWithShift: function(offset) { + var newSelection = this._selectionDirection === 'left' + ? this.selectionStart + offset + : this.selectionEnd + offset; + this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); + return offset !== 0; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorWithoutShift: function(offset) { + if (offset < 0) { + this.selectionStart += offset; + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += offset; + this.selectionStart = this.selectionEnd; + } + return offset !== 0; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorLeftOrRight('Left', e); + }, + + /** + * @private + * @return {Boolean} true if a change happened + */ + _move: function(e, prop, direction) { + var newValue; + if (e.altKey) { + newValue = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += direction === 'Left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + return this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + var change = true; + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) { + change = this._moveLeft(e, 'selectionStart'); + + } + this.selectionEnd = this.selectionStart; + return change; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + return this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0){ + this._selectionDirection = 'left'; + return this._moveLeft(e, 'selectionStart'); + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { + return; + } + this._moveCursorLeftOrRight('Right', e); + }, + + /** + * Moves cursor right or Left, fires event + * @param {String} direction 'Left', 'Right' + * @param {Event} e Event object + */ + _moveCursorLeftOrRight: function(direction, e) { + var actionName = 'moveCursor' + direction + 'With'; + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + actionName += 'Shift'; + } + else { + actionName += 'outShift'; + } + if (this[actionName](e)) { + this.abortCursorAnimation(); + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + return this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + return this._moveRight(e, 'selectionEnd'); + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e Event object + */ + moveCursorRightWithoutShift: function(e) { + var changed = true; + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + return changed; + }, + + /** + * Removes characters from start/end + * start/end ar per grapheme position in _text array. + * + * @param {Number} start + * @param {Number} end default to start + 1 + */ + removeChars: function(start, end) { + if (typeof end === 'undefined') { + end = start + 1; + } + this.removeStyleFromTo(start, end); + this._text.splice(start, end - start); + this.text = this._text.join(''); + this.set('dirty', true); + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this._removeExtraneousStyles(); + }, + + /** + * insert characters at start position, before start position. + * start equal 1 it means the text get inserted between actual grapheme 0 and 1 + * if style array is provided, it must be as the same length of text in graphemes + * if end is provided and is bigger than start, old text is replaced. + * start/end ar per grapheme position in _text array. + * + * @param {String} text text to insert + * @param {Array} style array of style objects + * @param {Number} start + * @param {Number} end default to start + 1 + */ + insertChars: function(text, style, start, end) { + if (typeof end === 'undefined') { + end = start; + } + if (end > start) { + this.removeStyleFromTo(start, end); + } + var graphemes = fabric.util.string.graphemeSplit(text); + this.insertNewStyleBlock(graphemes, start, style); + this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); + this.text = this._text.join(''); + this.set('dirty', true); + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this._removeExtraneousStyles(); + }, + +}); + + +/* _TO_SVG_START_ */ +(function() { + var toFixed = fabric.util.toFixed, + multipleSpacesRegex = / +/g; + + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function() { + var offsets = this._getSVGLeftTopOffsets(), + textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); + return this._wrapSVGTextAndBg(textAndBg); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + return this._createBaseSVGMarkup( + this._toSVG(), + { reviver: reviver, noStyle: true, withShadow: true } + ); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function() { + return { + textLeft: -this.width / 2, + textTop: -this.height / 2, + lineTop: this.getHeightOfLine(0) + }; + }, + + /** + * @private + */ + _wrapSVGTextAndBg: function(textAndBg) { + var noShadow = true, + textDecoration = this.getSvgTextDecoration(this); + return [ + textAndBg.textBgRects.join(''), + '\t\t', + textAndBg.textSpans.join(''), + '\n' + ]; + }, + + /** + * @private + * @param {Number} textTopOffset Text top offset + * @param {Number} textLeftOffset Text left offset + * @return {Object} + */ + _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { + var textSpans = [], + textBgRects = [], + height = textTopOffset, lineOffset; + // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineOffset = this._getLineLeftOffset(i); + if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { + this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); + } + this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height); + height += this.getHeightOfLine(i); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, left, top) { + var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex), + styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace), + fillStyles = styleProps ? 'style="' + styleProps + '"' : '', + dy = styleDecl.deltaY, dySpan = '', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + if (dy) { + dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" '; + } + return [ + '', + fabric.util.string.escapeXml(_char), + '' + ].join(''); + }, + + _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, style, + boxWidth = 0, + line = this._textLines[lineIndex], + timeToRender; + + textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight; + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + textLeftOffset += charBox.kernedWidth - charBox.width; + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle); + } + if (timeToRender) { + style = this._getStyleDeclaration(lineIndex, i) || { }; + textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); + charsToRender = ''; + actualStyle = nextStyle; + textLeftOffset += boxWidth; + boxWidth = 0; + } + } + }, + + _pushTextBgRect: function(textBgRects, color, left, top, width, height) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + }, + + _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) { + var line = this._textLines[i], + heightOfLine = this.getHeightOfLine(i) / this.lineHeight, + boxWidth = 0, + boxStart = 0, + charBox, currentColor, + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (currentColor !== lastColor) { + lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart, + textTopOffset, boxWidth, heightOfLine); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart, + textTopOffset, boxWidth, heightOfLine); + }, + + /** + * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + * + * @private + * @param {*} value + * @return {String} + */ + _getFillAttributes: function(value) { + var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0, lastHeight = 0; + for (var j = 0; j < lineIndex; j++) { + lineTopOffset += this.getHeightOfLine(j); + } + lastHeight = this.getHeightOfLine(j); + return { + lineTop: lineTopOffset, + offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) + }; + }, + + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow); + return svgStyle + ' white-space: pre;'; + }, + }); +})(); +/* _TO_SVG_END_ */ + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = {}); + + /** + * Textbox class, based on IText, allows the user to resize the text rectangle + * and wraps lines automatically. Textboxes have their Y scaling locked, the + * user can only change width. Height is adjusted automatically based on the + * wrapping of lines. + * @class fabric.Textbox + * @extends fabric.IText + * @mixes fabric.Observable + * @return {fabric.Textbox} thisArg + * @see {@link fabric.Textbox#initialize} for constructor definition + */ + fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, { + + /** + * Type of an object + * @type String + * @default + */ + type: 'textbox', + + /** + * Minimum width of textbox, in pixels. + * @type Number + * @default + */ + minWidth: 20, + + /** + * Minimum calculated width of a textbox, in pixels. + * fixed to 2 so that an empty textbox cannot go to 0 + * and is still selectable without text. + * @type Number + * @default + */ + dynamicMinWidth: 2, + + /** + * Cached array of text wrapping. + * @type Array + */ + __cachedLines: null, + + /** + * Override standard Object class values + */ + lockScalingFlip: true, + + /** + * Override standard Object class values + * Textbox needs this on false + */ + noScaleCache: false, + + /** + * Properties which when set cause object to change dimensions + * @type Object + * @private + */ + _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'), + + /** + * Use this regular expression to split strings in breakable lines + * @private + */ + _wordJoiners: /[ \t\r]/, + + /** + * Use this boolean property in order to split strings that have no white space concept. + * this is a cheap way to help with chinese/japanese + * @type Boolean + * @since 2.6.0 + */ + splitByGrapheme: false, + + /** + * Unlike superclass's version of this function, Textbox does not update + * its width. + * @private + * @override + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this._clearCache(); + // clear dynamicMinWidth as it will be different after we re-wrap line + this.dynamicMinWidth = 0; + // wrap lines + this._styleMap = this._generateStyleMap(this._splitText()); + // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap + if (this.dynamicMinWidth > this.width) { + this._set('width', this.dynamicMinWidth); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + // clear cache and re-calculate height + this.height = this.calcTextHeight(); + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Generate an object that translates the style object so that it is + * broken up by visual lines (new lines and automatic wrapping). + * The original text styles object is broken up by actual lines (new lines only), + * which is only sufficient for Text / IText + * @private + */ + _generateStyleMap: function(textInfo) { + var realLineCount = 0, + realLineCharCount = 0, + charCount = 0, + map = {}; + + for (var i = 0; i < textInfo.graphemeLines.length; i++) { + if (textInfo.graphemeText[charCount] === '\n' && i > 0) { + realLineCharCount = 0; + charCount++; + realLineCount++; + } + else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) { + // this case deals with space's that are removed from end of lines when wrapping + realLineCharCount++; + charCount++; + } + + map[i] = { line: realLineCount, offset: realLineCharCount }; + + charCount += textInfo.graphemeLines[i].length; + realLineCharCount += textInfo.graphemeLines[i].length; + } + + return map; + }, + + /** + * Returns true if object has a style property or has it on a specified line + * @param {Number} lineIndex + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (map) { + lineIndex = map.line; + } + } + return fabric.Text.prototype.styleHas.call(this, property, lineIndex); + }, + + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false, + map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1]; + if (map) { + lineIndex = map.line; + offset = map.offset; + } + if (mapNextLine) { + nextLineIndex = mapNextLine.line; + shouldLimit = nextLineIndex === lineIndex; + nextOffset = mapNextLine.offset; + } + obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + } + return true; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (!map) { + return null; + } + lineIndex = map.line; + charIndex = map.offset + charIndex; + } + return this.callSuper('_getStyleDeclaration', lineIndex, charIndex); + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + this.styles[lineIndex][charIndex] = style; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + delete this.styles[lineIndex][charIndex]; + }, + + /** + * probably broken need a fix + * Returns the real style line that correspond to the wrapped lineIndex line + * Used just to verify if the line does exist or not. + * @param {Number} lineIndex + * @returns {Boolean} if the line exists or not + * @private + */ + _getLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + return !!this.styles[map.line]; + }, + + /** + * Set the line style to an empty object so that is initialized + * @param {Number} lineIndex + * @param {Object} style + * @private + */ + _setLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + this.styles[map.line] = {}; + }, + + /** + * Wraps text using the 'width' property of Textbox. First this function + * splits text on newlines, so we preserve newlines entered by the user. + * Then it wraps each line using the width of the Textbox by calling + * _wrapLine(). + * @param {Array} lines The string array of text that is split into lines + * @param {Number} desiredWidth width you want to wrap to + * @returns {Array} Array of lines + */ + _wrapText: function(lines, desiredWidth) { + var wrapped = [], i; + this.isWrapping = true; + for (i = 0; i < lines.length; i++) { + wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); + } + this.isWrapping = false; + return wrapped; + }, + + /** + * Helper function to measure a string of text, given its lineIndex and charIndex offset + * it gets called when charBounds are not available yet. + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {number} lineIndex + * @param {number} charOffset + * @returns {number} + * @private + */ + _measureWord: function(word, lineIndex, charOffset) { + var width = 0, prevGrapheme, skipLeft = true; + charOffset = charOffset || 0; + for (var i = 0, len = word.length; i < len; i++) { + var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft); + width += box.kernedWidth; + prevGrapheme = word[i]; + } + return width; + }, + + /** + * Wraps a line of text using the width of the Textbox and a context. + * @param {Array} line The grapheme array that represent the line + * @param {Number} lineIndex + * @param {Number} desiredWidth width you want to wrap the line to + * @param {Number} reservedSpace space to remove from wrapping for custom functionalities + * @returns {Array} Array of line(s) into which the given text is wrapped + * to. + */ + _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) { + var lineWidth = 0, + splitByGrapheme = this.splitByGrapheme, + graphemeLines = [], + line = [], + // spaces in different languages? + words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), + word = '', + offset = 0, + infix = splitByGrapheme ? '' : ' ', + wordWidth = 0, + infixWidth = 0, + largestWordWidth = 0, + lineJustStarted = true, + additionalSpace = this._getWidthOfCharSpacing(), + reservedSpace = reservedSpace || 0; + // fix a difference between split and graphemeSplit + if (words.length === 0) { + words.push([]); + } + desiredWidth -= reservedSpace; + for (var i = 0; i < words.length; i++) { + // if using splitByGrapheme words are already in graphemes. + word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); + wordWidth = this._measureWord(word, lineIndex, offset); + offset += word.length; + + lineWidth += infixWidth + wordWidth - additionalSpace; + if (lineWidth > desiredWidth && !lineJustStarted) { + graphemeLines.push(line); + line = []; + lineWidth = wordWidth; + lineJustStarted = true; + } + else { + lineWidth += additionalSpace; + } + + if (!lineJustStarted && !splitByGrapheme) { + line.push(infix); + } + line = line.concat(word); + + infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); + offset++; + lineJustStarted = false; + // keep track of largest word + if (wordWidth > largestWordWidth) { + largestWordWidth = wordWidth; + } + } + + i && graphemeLines.push(line); + + if (largestWordWidth + reservedSpace > this.dynamicMinWidth) { + this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace; + } + return graphemeLines; + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @param {Number} lineIndex text to split + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + if (!this._styleMap[lineIndex + 1]) { + // is last line, return true; + return true; + } + if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) { + // this is last line before a line break, return true; + return true; + } + return false; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * @return Number + */ + missingNewlineOffset: function(lineIndex) { + if (this.splitByGrapheme) { + return this.isEndOfWrapping(lineIndex) ? 1 : 0; + } + return 1; + }, + + /** + * Gets lines of text to render in the Textbox. This function calculates + * text wrapping on the fly every time it is called. + * @param {String} text text to split + * @returns {Array} Array of lines in the Textbox. + * @override + */ + _splitTextIntoLines: function(text) { + var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text), + graphemeLines = this._wrapText(newText.lines, this.width), + lines = new Array(graphemeLines.length); + for (var i = 0; i < graphemeLines.length; i++) { + lines[i] = graphemeLines[i].join(''); + } + newText.lines = lines; + newText.graphemeLines = graphemeLines; + return newText; + }, + + getMinWidth: function() { + return Math.max(this.minWidth, this.dynamicMinWidth); + }, + + _removeExtraneousStyles: function() { + var linesToKeep = {}; + for (var prop in this._styleMap) { + if (this._textLines[prop]) { + linesToKeep[this._styleMap[prop].line] = 1; + } + } + for (var prop in this.styles) { + if (!linesToKeep[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)); + } + }); + + /** + * Returns fabric.Textbox instance from an object representation + * @static + * @memberOf fabric.Textbox + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created + */ + fabric.Textbox.fromObject = function(object, callback) { + return fabric.Object._fromObject('Textbox', object, callback, 'text'); + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var controlsUtils = fabric.controlsUtils, + scaleSkewStyleHandler = controlsUtils.scaleSkewCursorStyleHandler, + scaleStyleHandler = controlsUtils.scaleCursorStyleHandler, + scalingEqually = controlsUtils.scalingEqually, + scalingYOrSkewingX = controlsUtils.scalingYOrSkewingX, + scalingXOrSkewingY = controlsUtils.scalingXOrSkewingY, + scaleOrSkewActionName = controlsUtils.scaleOrSkewActionName, + objectControls = fabric.Object.prototype.controls; + + objectControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mb = new fabric.Control({ + x: 0, + y: 0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mt = new fabric.Control({ + x: 0, + y: -0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); + + objectControls.tl = new fabric.Control({ + x: -0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.tr = new fabric.Control({ + x: 0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.bl = new fabric.Control({ + x: -0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.br = new fabric.Control({ + x: 0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.mtr = new fabric.Control({ + x: 0, + y: -0.5, + actionHandler: controlsUtils.rotationWithSnapping, + cursorStyleHandler: controlsUtils.rotationStyleHandler, + offsetY: -40, + withConnection: true, + actionName: 'rotate', + }); + + if (fabric.Textbox) { + // this is breaking the prototype inheritance, no time / ideas to fix it. + // is important to document that if you want to have all objects to have a + // specific custom control, you have to add it to Object prototype and to Textbox + // prototype. The controls are shared as references. So changes to control `tr` + // can still apply to all objects if needed. + var textBoxControls = fabric.Textbox.prototype.controls = { }; + + textBoxControls.mtr = objectControls.mtr; + textBoxControls.tr = objectControls.tr; + textBoxControls.br = objectControls.br; + textBoxControls.tl = objectControls.tl; + textBoxControls.bl = objectControls.bl; + textBoxControls.mt = objectControls.mt; + textBoxControls.mb = objectControls.mb; + + textBoxControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', + }); + + textBoxControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', + }); + } +})(); + diff --git a/frontend-web/vendor/js/fabric1.min.js b/frontend-web/vendor/js/fabric1.min.js new file mode 100644 index 0000000..58b0d82 --- /dev/null +++ b/frontend-web/vendor/js/fabric1.min.js @@ -0,0 +1,30786 @@ +/* build: `node build.js modules=ALL exclude=gestures,accessors,erasing requirejs minifier=uglifyjs` */ +/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: '4.6.0' }; +if (typeof exports !== 'undefined') { + exports.fabric = fabric; +} +/* _AMD_START_ */ +else if (typeof define === 'function' && define.amd) { + define([], function() { return fabric; }); +} +/* _AMD_END_ */ +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + if (document instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document)) { + fabric.document = document; + } + else { + fabric.document = document.implementation.createHTMLDocument(''); + } + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + var jsdom = require('jsdom'); + var virtualWindow = new jsdom.JSDOM( + decodeURIComponent('%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E'), + { + features: { + FetchExternalResources: ['img'] + }, + resources: 'usable' + }).window; + fabric.document = virtualWindow.document; + fabric.jsdomImplForWrapper = require('jsdom/lib/jsdom/living/generated/utils').implForWrapper; + fabric.nodeCanvas = require('jsdom/lib/jsdom/utils').Canvas; + fabric.window = virtualWindow; + DOMParser = fabric.window.DOMParser; +} + +/** + * True when in environment that supports touch events + * @type boolean + */ +fabric.isTouchSupported = 'ontouchstart' in fabric.window || 'ontouchstart' in fabric.document || + (fabric.window && fabric.window.navigator && fabric.window.navigator.maxTouchPoints > 0); + +/** + * True when in environment that's probably Node.js + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; + +/* _FROM_SVG_START_ */ +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + 'display', + 'transform', + 'fill', 'fill-opacity', 'fill-rule', + 'opacity', + 'stroke', 'stroke-dasharray', 'stroke-linecap', 'stroke-dashoffset', + 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-opacity', 'stroke-width', + 'id', 'paint-order', 'vector-effect', + 'instantiated_by_use', 'clip-path', +]; +/* _FROM_SVG_END_ */ + +/** + * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. + */ +fabric.DPI = 96; +fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)'; +fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'; +fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig; +fabric.reNonWord = /[ \n\.,;!\?\-]/; +fabric.fontPaths = { }; +fabric.iMatrix = [1, 0, 0, 1, 0, 0]; +fabric.svgNS = 'http://www.w3.org/2000/svg'; + +/** + * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine. + * @since 1.7.14 + * @type Number + * @default + */ +fabric.perfLimitSizeTotal = 2097152; + +/** + * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000 + * @since 1.7.14 + * @type Number + * @default + */ +fabric.maxCacheSideLimit = 4096; + +/** + * Lowest pixel limit for cache canvases, set at 256PX + * @since 1.7.14 + * @type Number + * @default + */ +fabric.minCacheSideLimit = 256; + +/** + * Cache Object for widths of chars in text rendering. + */ +fabric.charWidthsCache = { }; + +/** + * if webgl is enabled and available, textureSize will determine the size + * of the canvas backend + * @since 2.0.0 + * @type Number + * @default + */ +fabric.textureSize = 2048; + +/** + * When 'true', style information is not retained when copy/pasting text, making + * pasted text use destination style. + * Defaults to 'false'. + * @type Boolean + * @default + */ +fabric.disableStyleCopyPaste = false; + +/** + * Enable webgl for filtering picture is available + * A filtering backend will be initialized, this will both take memory and + * time since a default 2048x2048 canvas will be created for the gl context + * @since 2.0.0 + * @type Boolean + * @default + */ +fabric.enableGLFiltering = true; + +/** + * Device Pixel Ratio + * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html + */ +fabric.devicePixelRatio = fabric.window.devicePixelRatio || + fabric.window.webkitDevicePixelRatio || + fabric.window.mozDevicePixelRatio || + 1; +/** + * Browser-specific constant to adjust CanvasRenderingContext2D.shadowBlur value, + * which is unitless and not rendered equally across browsers. + * + * Values that work quite well (as of October 2017) are: + * - Chrome: 1.5 + * - Edge: 1.75 + * - Firefox: 0.9 + * - Safari: 0.95 + * + * @since 2.0.0 + * @type Number + * @default 1 + */ +fabric.browserShadowBlurConstant = 1; + +/** + * This object contains the result of arc to bezier conversion for faster retrieving if the same arc needs to be converted again. + * It was an internal variable, is accessible since version 2.3.4 + */ +fabric.arcToSegmentsCache = { }; + +/** + * This object keeps the results of the boundsOfCurve calculation mapped by the joined arguments necessary to calculate it. + * It does speed up calculation, if you parse and add always the same paths, but in case of heavy usage of freedrawing + * you do not get any speed benefit and you get a big object in memory. + * The object was a private variable before, while now is appended to the lib so that you have access to it and you + * can eventually clear it. + * It was an internal variable, is accessible since version 2.3.4 + */ +fabric.boundsOfCurveCache = { }; + +/** + * If disabled boundsOfCurveCache is not used. For apps that make heavy usage of pencil drawing probably disabling it is better + * @default true + */ +fabric.cachesBoundsOfCurve = true; + +/** + * Skip performance testing of setupGLContext and force the use of putImageData that seems to be the one that works best on + * Chrome + old hardware. if your users are experiencing empty images after filtering you may try to force this to true + * this has to be set before instantiating the filtering backend ( before filtering the first image ) + * @type Boolean + * @default false + */ +fabric.forceGLPutImageData = false; + +fabric.initFilterBackend = function() { + if (fabric.enableGLFiltering && fabric.isWebglSupported && fabric.isWebglSupported(fabric.textureSize)) { + console.log('max texture size: ' + fabric.maxTextureSize); + return (new fabric.WebglFilterBackend({ tileSize: fabric.textureSize })); + } + else if (fabric.Canvas2dFilterBackend) { + return (new fabric.Canvas2dFilterBackend()); + } +}; + + +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) + window.fabric = fabric; +} + + +(function() { + + /** + * @private + * @param {String} eventName + * @param {Function} handler + */ + function _removeEventListener(eventName, handler) { + if (!this.__eventListeners[eventName]) { + return; + } + var eventListener = this.__eventListeners[eventName]; + if (handler) { + eventListener[eventListener.indexOf(handler)] = false; + } + else { + fabric.util.array.fill(eventListener, false); + } + } + + /** + * Observes specified event + * @memberOf fabric.Observable + * @alias on + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function that receives a notification when an event of the specified type occurs + * @return {Self} thisArg + * @chainable + */ + function on(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = []; + } + this.__eventListeners[eventName].push(handler); + } + return this; + } + + function _once(eventName, handler) { + var _handler = function () { + handler.apply(this, arguments); + this.off(eventName, _handler); + }.bind(this); + this.on(eventName, _handler); + } + + function once(eventName, handler) { + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + _once.call(this, prop, eventName[prop]); + } + } + else { + _once.call(this, eventName, handler); + } + return this; + } + + /** + * Stops event observing for a particular event handler. Calling this method + * without arguments removes all handlers for all events + * @memberOf fabric.Observable + * @alias off + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function to be deleted from EventListeners + * @return {Self} thisArg + * @chainable + */ + function off(eventName, handler) { + if (!this.__eventListeners) { + return this; + } + + // remove all key/value pairs (event name -> event handler) + if (arguments.length === 0) { + for (eventName in this.__eventListeners) { + _removeEventListener.call(this, eventName); + } + } + // one object with key/value pairs was passed + else if (arguments.length === 1 && typeof arguments[0] === 'object') { + for (var prop in eventName) { + _removeEventListener.call(this, prop, eventName[prop]); + } + } + else { + _removeEventListener.call(this, eventName, handler); + } + return this; + } + + /** + * Fires event with an optional options object + * @memberOf fabric.Observable + * @param {String} eventName Event name to fire + * @param {Object} [options] Options object + * @return {Self} thisArg + * @chainable + */ + function fire(eventName, options) { + if (!this.__eventListeners) { + return this; + } + + var listenersForEvent = this.__eventListeners[eventName]; + if (!listenersForEvent) { + return this; + } + + for (var i = 0, len = listenersForEvent.length; i < len; i++) { + listenersForEvent[i] && listenersForEvent[i].call(this, options || { }); + } + this.__eventListeners[eventName] = listenersForEvent.filter(function(value) { + return value !== false; + }); + return this; + } + + /** + * @namespace fabric.Observable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events} + * @see {@link http://fabricjs.com/events|Events demo} + */ + fabric.Observable = { + fire: fire, + on: on, + once: once, + off: off, + }; +})(); + + +/** + * @namespace fabric.Collection + */ +fabric.Collection = { + + _objects: [], + + /** + * Adds objects to collection, Canvas or Group, then renders canvas + * (if `renderOnAddRemove` is not `false`). + * in case of Group no changes to bounding box are made. + * Objects should be instances of (or inherit from) fabric.Object + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the add method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + if (this._onObjectAdded) { + for (var i = 0, length = arguments.length; i < length; i++) { + this._onObjectAdded(arguments[i]); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) + * An object should be an instance of (or inherit from) fabric.Object + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the insertAt method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {Object} object Object to insert + * @param {Number} index Index to insert object at + * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs + * @return {Self} thisArg + * @chainable + */ + insertAt: function (object, index, nonSplicing) { + var objects = this._objects; + if (nonSplicing) { + objects[index] = object; + } + else { + objects.splice(index, 0, object); + } + this._onObjectAdded && this._onObjectAdded(object); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + remove: function() { + var objects = this._objects, + index, somethingRemoved = false; + + for (var i = 0, length = arguments.length; i < length; i++) { + index = objects.indexOf(arguments[i]); + + // only call onObjectRemoved if an object was actually removed + if (index !== -1) { + somethingRemoved = true; + objects.splice(index, 1); + this._onObjectRemoved && this._onObjectRemoved(arguments[i]); + } + } + + this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); + return this; + }, + + /** + * Executes given function for each object in this group + * @param {Function} callback + * Callback invoked with current object as first argument, + * index - as second and an array of all objects - as third. + * Callback is invoked in a context of Global Object (e.g. `window`) + * when no `context` argument is given + * + * @param {Object} context Context (aka thisObject) + * @return {Self} thisArg + * @chainable + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(); + for (var i = 0, len = objects.length; i < len; i++) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Returns an array of children objects of this instance + * Type parameter introduced in 1.3.10 + * since 2.3.5 this method return always a COPY of the array; + * @param {String} [type] When specified, only objects of this type are returned + * @return {Array} + */ + getObjects: function(type) { + if (typeof type === 'undefined') { + return this._objects.concat(); + } + return this._objects.filter(function(o) { + return o.type === type; + }); + }, + + /** + * Returns object at specified index + * @param {Number} index + * @return {Self} thisArg + */ + item: function (index) { + return this._objects[index]; + }, + + /** + * Returns true if collection contains no objects + * @return {Boolean} true if collection is empty + */ + isEmpty: function () { + return this._objects.length === 0; + }, + + /** + * Returns a size of a collection (i.e: length of an array containing its objects) + * @return {Number} Collection size + */ + size: function() { + return this._objects.length; + }, + + /** + * Returns true if collection contains an object + * @param {Object} object Object to check against + * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` + * @return {Boolean} `true` if collection contains an object + */ + contains: function (object, deep) { + if (this._objects.indexOf(object) > -1) { + return true; + } + else if (deep) { + return this._objects.some(function (obj) { + return typeof obj.contains === 'function' && obj.contains(object, true); + }); + } + return false; + }, + + /** + * Returns number representation of a collection complexity + * @return {Number} complexity + */ + complexity: function () { + return this._objects.reduce(function (memo, current) { + memo += current.complexity ? current.complexity() : 0; + return memo; + }, 0); + } +}; + + +/** + * @namespace fabric.CommonMethods + */ +fabric.CommonMethods = { + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + _setOptions: function(options) { + for (var prop in options) { + this.set(prop, options[prop]); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Gradient to + */ + _initGradient: function(filler, property) { + if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { + this.set(property, new fabric.Gradient(filler)); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Pattern to + * @param {Function} [callback] callback to invoke after pattern load + */ + _initPattern: function(filler, property, callback) { + if (filler && filler.source && !(filler instanceof fabric.Pattern)) { + this.set(property, new fabric.Pattern(filler, callback)); + } + else { + callback && callback(); + } + }, + + /** + * @private + */ + _setObject: function(obj) { + for (var prop in obj) { + this._set(prop, obj[prop]); + } + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + this._setObject(key); + } + else { + this._set(key, value); + } + return this; + }, + + _set: function(key, value) { + this[key] = value; + }, + + /** + * Toggles specified property from `true` to `false` or from `false` to `true` + * @param {String} property Property to toggle + * @return {fabric.Object} thisArg + * @chainable + */ + toggle: function(property) { + var value = this.get(property); + if (typeof value === 'boolean') { + this.set(property, !value); + } + return this; + }, + + /** + * Basic getter + * @param {String} property Property name + * @return {*} value of a property + */ + get: function(property) { + return this[property]; + } +}; + + +(function(global) { + + var sqrt = Math.sqrt, + atan2 = Math.atan2, + pow = Math.pow, + PiBy180 = Math.PI / 180, + PiBy2 = Math.PI / 2; + + /** + * @namespace fabric.util + */ + fabric.util = { + + /** + * Calculate the cos of an angle, avoiding returning floats for known results + * @static + * @memberOf fabric.util + * @param {Number} angle the angle in radians or in degree + * @return {Number} + */ + cos: function(angle) { + if (angle === 0) { return 1; } + if (angle < 0) { + // cos(a) = cos(-a) + angle = -angle; + } + var angleSlice = angle / PiBy2; + switch (angleSlice) { + case 1: case 3: return 0; + case 2: return -1; + } + return Math.cos(angle); + }, + + /** + * Calculate the sin of an angle, avoiding returning floats for known results + * @static + * @memberOf fabric.util + * @param {Number} angle the angle in radians or in degree + * @return {Number} + */ + sin: function(angle) { + if (angle === 0) { return 0; } + var angleSlice = angle / PiBy2, sign = 1; + if (angle < 0) { + // sin(-a) = -sin(a) + sign = -1; + } + switch (angleSlice) { + case 1: return sign; + case 2: return 0; + case 3: return -sign; + } + return Math.sin(angle); + }, + + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @param {Array} array + * @param {*} value + * @return {Array} original array + */ + removeFromArray: function(array, value) { + var idx = array.indexOf(value); + if (idx !== -1) { + array.splice(idx, 1); + } + return array; + }, + + /** + * Returns random number between 2 specified ones. + * @static + * @memberOf fabric.util + * @param {Number} min lower limit + * @param {Number} max upper limit + * @return {Number} random value (between min and max) + */ + getRandomInt: function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, + + /** + * Transforms degrees to radians. + * @static + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + degreesToRadians: function(degrees) { + return degrees * PiBy180; + }, + + /** + * Transforms radians to degrees. + * @static + * @memberOf fabric.util + * @param {Number} radians value in radians + * @return {Number} value in degrees + */ + radiansToDegrees: function(radians) { + return radians / PiBy180; + }, + + /** + * Rotates `point` around `origin` with `radians` + * @static + * @memberOf fabric.util + * @param {fabric.Point} point The point to rotate + * @param {fabric.Point} origin The origin of the rotation + * @param {Number} radians The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), + v = fabric.util.rotateVector(newPoint, radians); + return new fabric.Point(v.x, v.y).addEquals(origin); + }, + + /** + * Rotates `vector` with `radians` + * @static + * @memberOf fabric.util + * @param {Object} vector The vector to rotate (x and y) + * @param {Number} radians The radians of the angle for the rotation + * @return {Object} The new rotated point + */ + rotateVector: function(vector, radians) { + var sin = fabric.util.sin(radians), + cos = fabric.util.cos(radians), + rx = vector.x * cos - vector.y * sin, + ry = vector.x * sin + vector.y * cos; + return { + x: rx, + y: ry + }; + }, + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[2] * p.y, + t[1] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[2] * p.y + t[4], + t[1] * p.x + t[3] * p.y + t[5] + ); + }, + + /** + * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @param {Array} points 4 points array + * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix + * @return {Object} Object with left, top, width, height properties + */ + makeBoundingBoxFromPoints: function(points, transform) { + if (transform) { + for (var i = 0; i < points.length; i++) { + points[i] = fabric.util.transformPoint(points[i], transform); + } + } + var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], + minX = fabric.util.array.min(xPoints), + maxX = fabric.util.array.max(xPoints), + width = maxX - minX, + yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], + minY = fabric.util.array.min(yPoints), + maxY = fabric.util.array.max(yPoints), + height = maxY - minY; + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var a = 1 / (t[0] * t[3] - t[1] * t[2]), + r = [a * t[3], -a * t[1], -a * t[2], a * t[0]], + o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true); + r[4] = -o.x; + r[5] = -o.y; + return r; + }, + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @memberOf fabric.util + * @param {Number|String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, + + /** + * Converts from attribute value to pixel value if applicable. + * Returns converted pixels or original value not converted. + * @param {Number|String} value number to operate on + * @param {Number} fontSize + * @return {Number|String} + */ + parseUnit: function(value, fontSize) { + var unit = /\D{0,2}$/.exec(value), + number = parseFloat(value); + if (!fontSize) { + fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + switch (unit[0]) { + case 'mm': + return number * fabric.DPI / 25.4; + + case 'cm': + return number * fabric.DPI / 2.54; + + case 'in': + return number * fabric.DPI; + + case 'pt': + return number * fabric.DPI / 72; // or * 4 / 3 + + case 'pc': + return number * fabric.DPI / 72 * 12; // or * 16 + + case 'em': + return number * fontSize; + + default: + return number; + } + }, + + /** + * Function which always returns `false`. + * @static + * @memberOf fabric.util + * @return {Boolean} + */ + falseFunction: function() { + return false; + }, + + /** + * Returns klass "Class" object of given namespace + * @memberOf fabric.util + * @param {String} type Type of object (eg. 'circle') + * @param {String} namespace Namespace to get klass "Class" object from + * @return {Object} klass "Class" + */ + getKlass: function(type, namespace) { + // capitalize first letter only + type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); + return fabric.util.resolveNamespace(namespace)[type]; + }, + + /** + * Returns array of attributes for given svg that fabric parses + * @memberOf fabric.util + * @param {String} type Type of svg element (eg. 'circle') + * @return {Array} string names of supported attributes + */ + getSvgAttributes: function(type) { + var attributes = [ + 'instantiated_by_use', + 'style', + 'id', + 'class' + ]; + switch (type) { + case 'linearGradient': + attributes = attributes.concat(['x1', 'y1', 'x2', 'y2', 'gradientUnits', 'gradientTransform']); + break; + case 'radialGradient': + attributes = attributes.concat(['gradientUnits', 'gradientTransform', 'cx', 'cy', 'r', 'fx', 'fy', 'fr']); + break; + case 'stop': + attributes = attributes.concat(['offset', 'stop-color', 'stop-opacity']); + break; + } + return attributes; + }, + + /** + * Returns object of given namespace + * @memberOf fabric.util + * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' + * @return {Object} Object for given namespace (default fabric) + */ + resolveNamespace: function(namespace) { + if (!namespace) { + return fabric; + } + + var parts = namespace.split('.'), + len = parts.length, i, + obj = global || fabric.window; + + for (i = 0; i < len; ++i) { + obj = obj[parts[i]]; + } + + return obj; + }, + + /** + * Loads image element from given url and passes it to a callback + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {*} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } + + var img = fabric.util.createImage(); + + /** @ignore */ + var onLoadCallback = function () { + callback && callback.call(context, img, false); + img = img.onload = img.onerror = null; + }; + + img.onload = onLoadCallback; + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + // crossOrigin null is the same as not set. + if (url.indexOf('data') !== 0 && + crossOrigin !== undefined && + crossOrigin !== null) { + img.crossOrigin = crossOrigin; + } + + // IE10 / IE11-Fix: SVG contents from data: URI + // will only be available if the IMG is present + // in the DOM (and visible) + if (url.substring(0,14) === 'data:image/svg') { + img.onload = null; + fabric.util.loadImageInDom(img, onLoadCallback); + } + + img.src = url; + }, + + /** + * Attaches SVG image with data: URL to the dom + * @memberOf fabric.util + * @param {Object} img Image object with data:image/svg src + * @param {Function} callback Callback; invoked with loaded image + * @return {Object} DOM element (div containing the SVG image) + */ + loadImageInDom: function(img, onLoadCallback) { + var div = fabric.document.createElement('div'); + div.style.width = div.style.height = '1px'; + div.style.left = div.style.top = '-100%'; + div.style.position = 'absolute'; + div.appendChild(img); + fabric.document.querySelector('body').appendChild(div); + /** + * Wrap in function to: + * 1. Call existing callback + * 2. Cleanup DOM + */ + img.onload = function () { + onLoadCallback(); + div.parentNode.removeChild(div); + div = null; + }; + }, + + /** + * Creates corresponding fabric instances from their object representations + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {String} namespace Namespace to get klass "Class" object from + * @param {Function} reviver Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || []; + + var enlivenedObjects = [], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects.filter(function(obj) { + // filter out undefined objects (objects that gave error) + return obj; + })); + } + } + + if (!numTotalObjects) { + callback && callback(enlivenedObjects); + return; + } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { + onLoaded(); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + klass.fromObject(o, function (obj, error) { + error || (enlivenedObjects[index] = obj); + reviver && reviver(o, obj, error); + onLoaded(); + }); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} patterns Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * called after each fabric object created. + */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; + + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); + } + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; + onLoaded(); + }); + } + else { + enlivenedPatterns[index] = p; + onLoaded(); + } + }); + }, + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @param {String} path Value to set sourcePath to + * @return {fabric.Object|fabric.Group} + */ + groupSVGElements: function(elements, options, path) { + var object; + if (elements && elements.length === 1) { + return elements[0]; + } + if (options) { + if (options.width && options.height) { + options.centerPoint = { + x: options.width / 2, + y: options.height / 2 + }; + } + else { + delete options.width; + delete options.height; + } + } + object = new fabric.Group(elements, options); + if (typeof path !== 'undefined') { + object.sourcePath = path; + } + return object; + }, + + /** + * Populates an object with properties of another object + * @static + * @memberOf fabric.util + * @param {Object} source Source object + * @param {Object} destination Destination object + * @return {Array} properties Properties names to include + */ + populateWithProperties: function(source, destination, properties) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + for (var i = 0, len = properties.length; i < len; i++) { + if (properties[i] in source) { + destination[properties[i]] = source[properties[i]]; + } + } + } + }, + + /** + * WARNING: THIS WAS TO SUPPORT OLD BROWSERS. deprecated. + * WILL BE REMOVED IN FABRIC 5.0 + * Draws a dashed line between two points + * + * This method is used to draw dashed line around selection area. + * See dotted stroke in canvas + * + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x start x coordinate + * @param {Number} y start y coordinate + * @param {Number} x2 end x coordinate + * @param {Number} y2 end y coordinate + * @param {Array} da dash array pattern + * @deprecated + */ + drawDashedLine: function(ctx, x, y, x2, y2, da) { + var dx = x2 - x, + dy = y2 - y, + len = sqrt(dx * dx + dy * dy), + rot = atan2(dy, dx), + dc = da.length, + di = 0, + draw = true; + + ctx.save(); + ctx.translate(x, y); + ctx.moveTo(0, 0); + ctx.rotate(rot); + + x = 0; + while (len > x) { + x += da[di++ % dc]; + if (x > len) { + x = len; + } + ctx[draw ? 'lineTo' : 'moveTo'](x, 0); + draw = !draw; + } + + ctx.restore(); + }, + + /** + * Creates canvas element + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element + */ + createCanvasElement: function() { + return fabric.document.createElement('canvas'); + }, + + /** + * Creates a canvas element that is a copy of another and is also painted + * @param {CanvasElement} canvas to copy size and content of + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element + */ + copyCanvasElement: function(canvas) { + var newCanvas = fabric.util.createCanvasElement(); + newCanvas.width = canvas.width; + newCanvas.height = canvas.height; + newCanvas.getContext('2d').drawImage(canvas, 0, 0); + return newCanvas; + }, + + /** + * since 2.6.0 moved from canvas instance to utility. + * @param {CanvasElement} canvasEl to copy size and content of + * @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too + * @param {Number} quality <= 1 and > 0 + * @static + * @memberOf fabric.util + * @return {String} data url + */ + toDataURL: function(canvasEl, format, quality) { + return canvasEl.toDataURL('image/' + format, quality); + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + createImage: function() { + return fabric.document.createElement('img'); + }, + + /** + * Multiply matrix A by matrix B to nest transformations + * @static + * @memberOf fabric.util + * @param {Array} a First transformMatrix + * @param {Array} b Second transformMatrix + * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(a, b, is2x2) { + // Matrix multiply a * b + return [ + a[0] * b[0] + a[2] * b[1], + a[1] * b[0] + a[3] * b[1], + a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], + is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], + is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] + ]; + }, + + /** + * Decomposes standard 2x3 matrix into transform components + * @static + * @memberOf fabric.util + * @param {Array} a transformMatrix + * @return {Object} Components of transform + */ + qrDecompose: function(a) { + var angle = atan2(a[1], a[0]), + denom = pow(a[0], 2) + pow(a[1], 2), + scaleX = sqrt(denom), + scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX, + skewX = atan2(a[0] * a[2] + a[1] * a [3], denom); + return { + angle: angle / PiBy180, + scaleX: scaleX, + scaleY: scaleY, + skewX: skewX / PiBy180, + skewY: 0, + translateX: a[4], + translateY: a[5] + }; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] angle in degrees + * @return {Number[]} transform matrix + */ + calcRotateMatrix: function(options) { + if (!options.angle) { + return fabric.iMatrix.concat(); + } + var theta = fabric.util.degreesToRadians(options.angle), + cos = fabric.util.cos(theta), + sin = fabric.util.sin(theta); + return [cos, sin, -sin, cos, 0, 0]; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet. + * is called DimensionsTransformMatrix because those properties are the one that influence + * the size of the resulting box of the object. + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @return {Number[]} transform matrix + */ + calcDimensionsMatrix: function(options) { + var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, + scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, + scaleMatrix = [ + options.flipX ? -scaleX : scaleX, + 0, + 0, + options.flipY ? -scaleY : scaleY, + 0, + 0], + multiply = fabric.util.multiplyTransformMatrices, + degreesToRadians = fabric.util.degreesToRadians; + if (options.skewX) { + scaleMatrix = multiply( + scaleMatrix, + [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], + true); + } + if (options.skewY) { + scaleMatrix = multiply( + scaleMatrix, + [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], + true); + } + return scaleMatrix; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @param {Number} [options.translateX] + * @param {Number} [options.translateY] + * @return {Number[]} transform matrix + */ + composeMatrix: function(options) { + var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], + multiply = fabric.util.multiplyTransformMatrices; + if (options.angle) { + matrix = multiply(matrix, fabric.util.calcRotateMatrix(options)); + } + if (options.scaleX !== 1 || options.scaleY !== 1 || + options.skewX || options.skewY || options.flipX || options.flipY) { + matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options)); + } + return matrix; + }, + + /** + * reset an object transform state to neutral. Top and left are not accounted for + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to transform + */ + resetObjectTransform: function (target) { + target.scaleX = 1; + target.scaleY = 1; + target.skewX = 0; + target.skewY = 0; + target.flipX = false; + target.flipY = false; + target.rotate(0); + }, + + /** + * Extract Object transform values + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to read from + * @return {Object} Components of transform + */ + saveObjectTransform: function (target) { + return { + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + angle: target.angle, + left: target.left, + flipX: target.flipX, + flipY: target.flipY, + top: target.top + }; + }, + + /** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Number} tolerance Tolerance + */ + isTransparent: function(ctx, x, y, tolerance) { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + var _isTransparent = true, i, temp, + imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1), + l = imageData.data.length; + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (i = 3; i < l; i += 4) { + temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) { + break; // Stop if colour found + } + } + + imageData = null; + + return _isTransparent; + }, + + /** + * Parse preserveAspectRatio attribute from element + * @param {string} attribute to be parsed + * @return {Object} an object containing align and meetOrSlice attribute + */ + parsePreserveAspectRatioAttribute: function(attribute) { + var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid', + aspectRatioAttrs = attribute.split(' '), align; + + if (aspectRatioAttrs && aspectRatioAttrs.length) { + meetOrSlice = aspectRatioAttrs.pop(); + if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') { + align = meetOrSlice; + meetOrSlice = 'meet'; + } + else if (aspectRatioAttrs.length) { + align = aspectRatioAttrs.pop(); + } + } + //divide align in alignX and alignY + alignX = align !== 'none' ? align.slice(1, 4) : 'none'; + alignY = align !== 'none' ? align.slice(5, 8) : 'none'; + return { + meetOrSlice: meetOrSlice, + alignX: alignX, + alignY: alignY + }; + }, + + /** + * Clear char widths cache for the given font family or all the cache if no + * fontFamily is specified. + * Use it if you know you are loading fonts in a lazy way and you are not waiting + * for custom fonts to load properly when adding text objects to the canvas. + * If a text object is added when its own font is not loaded yet, you will get wrong + * measurement and so wrong bounding boxes. + * After the font cache is cleared, either change the textObject text content or call + * initDimensions() to trigger a recalculation + * @memberOf fabric.util + * @param {String} [fontFamily] font family to clear + */ + clearFabricFontCache: function(fontFamily) { + fontFamily = (fontFamily || '').toLowerCase(); + if (!fontFamily) { + fabric.charWidthsCache = { }; + } + else if (fabric.charWidthsCache[fontFamily]) { + delete fabric.charWidthsCache[fontFamily]; + } + }, + + /** + * Given current aspect ratio, determines the max width and height that can + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Number} ar aspect ratio + * @param {Number} maximumArea Maximum area you want to achieve + * @return {Object.x} Limited dimensions by X + * @return {Object.y} Limited dimensions by Y + */ + limitDimsByArea: function(ar, maximumArea) { + var roughWidth = Math.sqrt(maximumArea * ar), + perfLimitSizeY = Math.floor(maximumArea / roughWidth); + return { x: Math.floor(roughWidth), y: perfLimitSizeY }; + }, + + capValue: function(min, value, max) { + return Math.max(min, Math.min(value, max)); + }, + + /** + * Finds the scale for the object source to fit inside the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to fit into destination + */ + findScaleToFit: function(source, destination) { + return Math.min(destination.width / source.width, destination.height / source.height); + }, + + /** + * Finds the scale for the object source to cover entirely the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to cover destination + */ + findScaleToCover: function(source, destination) { + return Math.max(destination.width / source.width, destination.height / source.height); + }, + + /** + * given an array of 6 number returns something like `"matrix(...numbers)"` + * @memberOf fabric.util + * @param {Array} transform an array with 6 numbers + * @return {String} transform matrix for svg + * @return {Object.y} Limited dimensions by Y + */ + matrixToSVG: function(transform) { + return 'matrix(' + transform.map(function(value) { + return fabric.util.toFixed(value, fabric.Object.NUM_FRACTION_DIGITS); + }).join(' ') + ')'; + }, + + /** + * given an object and a transform, apply the inverse transform to the object, + * this is equivalent to remove from that object that transformation, so that + * added in a space with the removed transform, the object will be the same as before. + * Removing from an object a transform that scale by 2 is like scaling it by 1/2. + * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg + * in the opposite direction. + * This util is used to add objects inside transformed groups or nested groups. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + removeTransformFromObject: function(object, transform) { + var inverted = fabric.util.invertTransform(transform), + finalTransform = fabric.util.multiplyTransformMatrices(inverted, object.calcOwnMatrix()); + fabric.util.applyTransformToObject(object, finalTransform); + }, + + /** + * given an object and a transform, apply the transform to the object. + * this is equivalent to change the space where the object is drawn. + * Adding to an object a transform that scale by 2 is like scaling it by 2. + * This is used when removing an object from an active selection for example. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + addTransformToObject: function(object, transform) { + fabric.util.applyTransformToObject( + object, + fabric.util.multiplyTransformMatrices(transform, object.calcOwnMatrix()) + ); + }, + + /** + * discard an object transform state and apply the one from the matrix. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + applyTransformToObject: function(object, transform) { + var options = fabric.util.qrDecompose(transform), + center = new fabric.Point(options.translateX, options.translateY); + object.flipX = false; + object.flipY = false; + object.set('scaleX', options.scaleX); + object.set('scaleY', options.scaleY); + object.skewX = options.skewX; + object.skewY = options.skewY; + object.angle = options.angle; + object.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * given a width and height, return the size of the bounding box + * that can contains the box with width/height with applied transform + * described in options. + * Use to calculate the boxes around objects for controls. + * @memberOf fabric.util + * @param {Number} width + * @param {Number} height + * @param {Object} options + * @param {Number} options.scaleX + * @param {Number} options.scaleY + * @param {Number} options.skewX + * @param {Number} options.skewY + * @return {Object.x} width of containing + * @return {Object.y} height of containing + */ + sizeAfterTransform: function(width, height, options) { + var dimX = width / 2, dimY = height / 2, + points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + transformMatrix = fabric.util.calcDimensionsMatrix(options), + bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); + return { + x: bbox.width, + y: bbox.height, + }; + } + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + var _join = Array.prototype.join, + commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }, + repeatedCommands = { + m: 'l', + M: 'L' + }; + function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { + var costh2 = fabric.util.cos(th2), + sinth2 = fabric.util.sin(th2), + costh3 = fabric.util.cos(th3), + sinth3 = fabric.util.sin(th3), + toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1, + toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1, + cp1X = fromX + mT * ( -cosTh * rx * sinth2 - sinTh * ry * costh2), + cp1Y = fromY + mT * ( -sinTh * rx * sinth2 + cosTh * ry * costh2), + cp2X = toX + mT * ( cosTh * rx * sinth3 + sinTh * ry * costh3), + cp2Y = toY + mT * ( sinTh * rx * sinth3 - cosTh * ry * costh3); + + return ['C', + cp1X, cp1Y, + cp2X, cp2Y, + toX, toY + ]; + } + + /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp + * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here + * http://mozilla.org/MPL/2.0/ + */ + function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) { + var PI = Math.PI, th = rotateX * PI / 180, + sinTh = fabric.util.sin(th), + cosTh = fabric.util.cos(th), + fromX = 0, fromY = 0; + + rx = Math.abs(rx); + ry = Math.abs(ry); + + var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5, + py = -cosTh * toY * 0.5 + sinTh * toX * 0.5, + rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px, + pl = rx2 * ry2 - rx2 * py2 - ry2 * px2, + root = 0; + + if (pl < 0) { + var s = Math.sqrt(1 - pl / (rx2 * ry2)); + rx *= s; + ry *= s; + } + else { + root = (large === sweep ? -1.0 : 1.0) * + Math.sqrt( pl / (rx2 * py2 + ry2 * px2)); + } + + var cx = root * rx * py / ry, + cy = -root * ry * px / rx, + cx1 = cosTh * cx - sinTh * cy + toX * 0.5, + cy1 = sinTh * cx + cosTh * cy + toY * 0.5, + mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry), + dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry); + + if (sweep === 0 && dtheta > 0) { + dtheta -= 2 * PI; + } + else if (sweep === 1 && dtheta < 0) { + dtheta += 2 * PI; + } + + // Convert into cubic bezier segments <= 90deg + var segments = Math.ceil(Math.abs(dtheta / PI * 2)), + result = [], mDelta = dtheta / segments, + mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2), + th3 = mTheta + mDelta; + + for (var i = 0; i < segments; i++) { + result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY); + fromX = result[i][5]; + fromY = result[i][6]; + mTheta = th3; + th3 += mDelta; + } + return result; + } + + /* + * Private + */ + function calcVectorAngle(ux, uy, vx, vy) { + var ta = Math.atan2(uy, ux), + tb = Math.atan2(vy, vx); + if (tb >= ta) { + return tb - ta; + } + else { + return 2 * Math.PI - (ta - tb); + } + } + + /** + * Calculate bounding box of a beziercurve + * @param {Number} x0 starting point + * @param {Number} y0 + * @param {Number} x1 first control point + * @param {Number} y1 + * @param {Number} x2 secondo control point + * @param {Number} y2 + * @param {Number} x3 end of bezier + * @param {Number} y3 + */ + // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. + // TODO: can we normalize this with the starting points set at 0 and then translated the bbox? + function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { + var argsString; + if (fabric.cachesBoundsOfCurve) { + argsString = _join.call(arguments); + if (fabric.boundsOfCurveCache[argsString]) { + return fabric.boundsOfCurveCache[argsString]; + } + } + + var sqrt = Math.sqrt, + min = Math.min, max = Math.max, + abs = Math.abs, tvalues = [], + bounds = [[], []], + a, b, c, t, t1, t2, b2ac, sqrtb2ac; + + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; + + for (var i = 0; i < 2; ++i) { + if (i > 0) { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } + + if (abs(a) < 1e-12) { + if (abs(b) < 1e-12) { + continue; + } + t = -c / b; + if (0 < t && t < 1) { + tvalues.push(t); + } + continue; + } + b2ac = b * b - 4 * c * a; + if (b2ac < 0) { + continue; + } + sqrtb2ac = sqrt(b2ac); + t1 = (-b + sqrtb2ac) / (2 * a); + if (0 < t1 && t1 < 1) { + tvalues.push(t1); + } + t2 = (-b - sqrtb2ac) / (2 * a); + if (0 < t2 && t2 < 1) { + tvalues.push(t2); + } + } + + var x, y, j = tvalues.length, jlen = j, mt; + while (j--) { + t = tvalues[j]; + mt = 1 - t; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; + + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; + } + + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; + var result = [ + { + x: min.apply(null, bounds[0]), + y: min.apply(null, bounds[1]) + }, + { + x: max.apply(null, bounds[0]), + y: max.apply(null, bounds[1]) + } + ]; + if (fabric.cachesBoundsOfCurve) { + fabric.boundsOfCurveCache[argsString] = result; + } + return result; + } + + /** + * Converts arc to a bunch of bezier curves + * @param {Number} fx starting point x + * @param {Number} fy starting point y + * @param {Array} coords Arc command + */ + function fromArcToBeziers(fx, fy, coords) { + var rx = coords[1], + ry = coords[2], + rot = coords[3], + large = coords[4], + sweep = coords[5], + tx = coords[6], + ty = coords[7], + segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segsNorm.length; i < len; i++) { + segsNorm[i][1] += fx; + segsNorm[i][2] += fy; + segsNorm[i][3] += fx; + segsNorm[i][4] += fy; + segsNorm[i][5] += fx; + segsNorm[i][6] += fy; + } + return segsNorm; + }; + + /** + * This function take a parsed SVG path and make it simpler for fabricJS logic. + * simplification consist of: only UPPERCASE absolute commands ( relative converted to absolute ) + * S converted in C, T converted in Q, A converted in C. + * @param {Array} path the array of commands of a parsed svg path for fabric.Path + * @return {Array} the simplified array of commands of a parsed svg path for fabric.Path + */ + function makePathSimpler(path) { + // x and y represent the last point of the path. the previous command point. + // we add them to each relative command to make it an absolute comment. + // we also swap the v V h H with L, because are easier to transform. + var x = 0, y = 0, len = path.length, + // x1 and y1 represent the last point of the subpath. the subpath is started with + // m or M command. When a z or Z command is drawn, x and y need to be resetted to + // the last x1 and y1. + x1 = 0, y1 = 0, current, i, converted, + // previous will host the letter of the previous command, to handle S and T. + // controlX and controlY will host the previous reflected control point + destinationPath = [], previous, controlX, controlY; + for (i = 0; i < len; ++i) { + converted = false; + current = path[i].slice(0); + switch (current[0]) { // first letter + case 'l': // lineto, relative + current[0] = 'L'; + current[1] += x; + current[2] += y; + // falls through + case 'L': + x = current[1]; + y = current[2]; + break; + case 'h': // horizontal lineto, relative + current[1] += x; + // falls through + case 'H': + current[0] = 'L'; + current[2] = y; + x = current[1]; + break; + case 'v': // vertical lineto, relative + current[1] += y; + // falls through + case 'V': + current[0] = 'L'; + y = current[1]; + current[1] = x; + current[2] = y; + break; + case 'm': // moveTo, relative + current[0] = 'M'; + current[1] += x; + current[2] += y; + // falls through + case 'M': + x = current[1]; + y = current[2]; + x1 = current[1]; + y1 = current[2]; + break; + case 'c': // bezierCurveTo, relative + current[0] = 'C'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + current[5] += x; + current[6] += y; + // falls through + case 'C': + controlX = current[3]; + controlY = current[4]; + x = current[5]; + y = current[6]; + break; + case 's': // shorthand cubic bezierCurveTo, relative + current[0] = 'S'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'S': + // would be sScC but since we are swapping sSc for C, we check just that. + if (previous === 'C') { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + x = current[3]; + y = current[4]; + current[0] = 'C'; + current[5] = current[3]; + current[6] = current[4]; + current[3] = current[1]; + current[4] = current[2]; + current[1] = controlX; + current[2] = controlY; + // current[3] and current[4] are NOW the second control point. + // we keep it for the next reflection. + controlX = current[3]; + controlY = current[4]; + break; + case 'q': // quadraticCurveTo, relative + current[0] = 'Q'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'Q': + controlX = current[1]; + controlY = current[2]; + x = current[3]; + y = current[4]; + break; + case 't': // shorthand quadraticCurveTo, relative + current[0] = 'T'; + current[1] += x; + current[2] += y; + // falls through + case 'T': + if (previous === 'Q') { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + current[0] = 'Q'; + x = current[1]; + y = current[2]; + current[1] = controlX; + current[2] = controlY; + current[3] = x; + current[4] = y; + break; + case 'a': + current[0] = 'A'; + current[6] += x; + current[7] += y; + // falls through + case 'A': + converted = true; + destinationPath = destinationPath.concat(fromArcToBeziers(x, y, current)); + x = current[6]; + y = current[7]; + break; + case 'z': + case 'Z': + x = x1; + y = y1; + break; + default: + } + if (!converted) { + destinationPath.push(current); + } + previous = current[0]; + } + return destinationPath; + }; + + /** + * Calc length from point x1,y1 to x2,y2 + * @param {Number} x1 starting point x + * @param {Number} y1 starting point y + * @param {Number} x2 starting point x + * @param {Number} y2 starting point y + * @return {Number} length of segment + */ + function calcLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + + // functions for the Cubic beizer + // taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350 + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + + function getPointOnCubicBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function(pct) { + var c1 = CB1(pct), c2 = CB2(pct), c3 = CB3(pct), c4 = CB4(pct); + return { + x: p4x * c1 + p3x * c2 + p2x * c3 + p1x * c4, + y: p4y * c1 + p3y * c2 + p2y * c3 + p1y * c4 + }; + }; + } + + function getTangentCubicIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (3 * invT * invT * (p2x - p1x)) + (6 * invT * pct * (p3x - p2x)) + + (3 * pct * pct * (p4x - p3x)), + tangentY = (3 * invT * invT * (p2y - p1y)) + (6 * invT * pct * (p3y - p2y)) + + (3 * pct * pct * (p4y - p3y)); + return Math.atan2(tangentY, tangentX); + }; + } + + function QB1(t) { + return t * t; + } + + function QB2(t) { + return 2 * t * (1 - t); + } + + function QB3(t) { + return (1 - t) * (1 - t); + } + + function getPointOnQuadraticBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function(pct) { + var c1 = QB1(pct), c2 = QB2(pct), c3 = QB3(pct); + return { + x: p3x * c1 + p2x * c2 + p1x * c3, + y: p3y * c1 + p2y * c2 + p1y * c3 + }; + }; + } + + function getTangentQuadraticIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (2 * invT * (p2x - p1x)) + (2 * pct * (p3x - p2x)), + tangentY = (2 * invT * (p2y - p1y)) + (2 * pct * (p3y - p2y)); + return Math.atan2(tangentY, tangentX); + }; + } + + + // this will run over a path segment ( a cubic or quadratic segment) and approximate it + // with 100 segemnts. This will good enough to calculate the length of the curve + function pathIterator(iterator, x1, y1) { + var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc; + for (perc = 1; perc <= 100; perc += 1) { + p = iterator(perc / 100); + tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y); + tempP = p; + } + return tmpLen; + } + + /** + * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1 + * that correspond to that pixels run over the path. + * The percentage will be then used to find the correct point on the canvas for the path. + * @param {Array} segInfo fabricJS collection of information on a parsed path + * @param {Number} distance from starting point, in pixels. + * @return {Object} info object with x and y ( the point on canvas ) and angle, the tangent on that point; + */ + function findPercentageForDistance(segInfo, distance) { + var perc = 0, tmpLen = 0, iterator = segInfo.iterator, tempP = { x: segInfo.x, y: segInfo.y }, + p, nextLen, nextStep = 0.01, angleFinder = segInfo.angleFinder, lastPerc; + // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100 + // the path + while (tmpLen < distance && perc <= 1 && nextStep > 0.0001) { + p = iterator(perc); + lastPerc = perc; + nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y); + // compare tmpLen each cycle with distance, decide next perc to test. + if ((nextLen + tmpLen) > distance) { + // we discard this step and we make smaller steps. + nextStep /= 2; + perc -= nextStep; + } + else { + tempP = p; + perc += nextStep; + tmpLen += nextLen; + } + } + p.angle = angleFinder(lastPerc); + return p; + } + + /** + * Run over a parsed and simplifed path and extrac some informations. + * informations are length of each command and starting point + * @param {Array} path fabricJS parsed path commands + * @return {Array} path commands informations + */ + function getPathSegmentsInfo(path) { + var totalLength = 0, len = path.length, current, + //x2 and y2 are the coords of segment start + //x1 and y1 are the coords of the current point + x1 = 0, y1 = 0, x2 = 0, y2 = 0, info = [], iterator, tempInfo, angleFinder; + for (var i = 0; i < len; i++) { + current = path[i]; + tempInfo = { + x: x1, + y: y1, + command: current[0], + }; + switch (current[0]) { //first letter + case 'M': + tempInfo.length = 0; + x2 = x1 = current[1]; + y2 = y1 = current[2]; + break; + case 'L': + tempInfo.length = calcLineLength(x1, y1, current[1], current[2]); + x1 = current[1]; + y1 = current[2]; + break; + case 'C': + iterator = getPointOnCubicBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + angleFinder = getTangentCubicIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[5]; + y1 = current[6]; + break; + case 'Q': + iterator = getPointOnQuadraticBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + angleFinder = getTangentQuadraticIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[3]; + y1 = current[4]; + break; + case 'Z': + case 'z': + // we add those in order to ease calculations later + tempInfo.destX = x2; + tempInfo.destY = y2; + tempInfo.length = calcLineLength(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + break; + } + totalLength += tempInfo.length; + info.push(tempInfo); + } + info.push({ length: totalLength, x: x1, y: y1 }); + return info; + } + + function getPointOnPath(path, distance, infos) { + if (!infos) { + infos = getPathSegmentsInfo(path); + } + var i = 0; + while ((distance - infos[i].length > 0) && i < (infos.length - 2)) { + distance -= infos[i].length; + i++; + } + // var distance = infos[infos.length - 1] * perc; + var segInfo = infos[i], segPercent = distance / segInfo.length, + command = segInfo.command, segment = path[i], info; + + switch (command) { + case 'M': + return { x: segInfo.x, y: segInfo.y, angle: 0 }; + case 'Z': + case 'z': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segInfo.destX, segInfo.destY), + segPercent + ); + info.angle = Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x); + return info; + case 'L': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segment[1], segment[2]), + segPercent + ); + info.angle = Math.atan2(segment[2] - segInfo.y, segment[1] - segInfo.x); + return info; + case 'C': + return findPercentageForDistance(segInfo, distance); + case 'Q': + return findPercentageForDistance(segInfo, distance); + } + } + + /** + * + * @param {string} pathString + * @return {(string|number)[][]} An array of SVG path commands + * @example Usage + * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [ + * ['M', 3, 4], + * ['Q', 3, 5, 2, 1, 4, 0], + * ['Q', 9, 12, 2, 1, 4, 0], + * ]; + * + */ + function parsePath(pathString) { + var result = [], + coords = [], + currentPath, + parsed, + re = fabric.rePathCommand, + rNumber = '[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?\\s*', + rNumberCommaWsp = '(' + rNumber + ')' + fabric.commaWsp, + rFlagCommaWsp = '([01])' + fabric.commaWsp + '?', + rArcSeq = rNumberCommaWsp + '?' + rNumberCommaWsp + '?' + rNumberCommaWsp + rFlagCommaWsp + rFlagCommaWsp + + rNumberCommaWsp + '?(' + rNumber + ')', + regArcArgumentSequence = new RegExp(rArcSeq, 'g'), + match, + coordsStr, + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + path; + if (!pathString || !pathString.match) { + return result; + } + path = pathString.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + for (var i = 0, coordsParsed, len = path.length; i < len; i++) { + currentPath = path[i]; + + coordsStr = currentPath.slice(1).trim(); + coords.length = 0; + + var command = currentPath.charAt(0); + coordsParsed = [command]; + + if (command.toLowerCase() === 'a') { + // arcs have special flags that apparently don't require spaces so handle special + for (var args; (args = regArcArgumentSequence.exec(coordsStr));) { + for (var j = 1; j < args.length; j++) { + coords.push(args[j]); + } + } + } + else { + while ((match = re.exec(coordsStr))) { + coords.push(match[0]); + } + } + + for (var j = 0, jlen = coords.length; j < jlen; j++) { + parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + coordsParsed.push(parsed); + } + } + + var commandLength = commandLengths[command.toLowerCase()], + repeatedCommand = repeatedCommands[command] || command; + + if (coordsParsed.length - 1 > commandLength) { + for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { + result.push([command].concat(coordsParsed.slice(k, k + commandLength))); + command = repeatedCommand; + } + } + else { + result.push(coordsParsed); + } + } + + return result; + }; + + /** + * + * Converts points to a smooth SVG path + * @param {{ x: number,y: number }[]} points Array of points + * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value. + * @return {(string|number)[][]} An array of SVG path commands + */ + function getSmoothPathFromPoints(points, correction) { + var path = [], i, + p1 = new fabric.Point(points[0].x, points[0].y), + p2 = new fabric.Point(points[1].x, points[1].y), + len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; + correction = correction || 0; + + if (manyPoints) { + multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; + multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; + } + path.push(['M', p1.x - multSignX * correction, p1.y - multSignY * correction]); + for (i = 1; i < len; i++) { + if (!p1.eq(p2)) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]); + } + p1 = points[i]; + if ((i + 1) < points.length) { + p2 = points[i + 1]; + } + } + if (manyPoints) { + multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; + multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; + } + path.push(['L', p1.x + multSignX * correction, p1.y + multSignY * correction]); + return path; + } + /** + * Transform a path by transforming each segment. + * it has to be a simplified path or it won't work. + * WARNING: this depends from pathOffset for correct operation + * @param {Array} path fabricJS parsed and simplified path commands + * @param {Array} transform matrix that represent the transformation + * @param {Object} [pathOffset] the fabric.Path pathOffset + * @param {Number} pathOffset.x + * @param {Number} pathOffset.y + * @returns {Array} the transformed path + */ + function transformPath(path, transform, pathOffset) { + if (pathOffset) { + transform = fabric.util.multiplyTransformMatrices( + transform, + [1, 0, 0, 1, -pathOffset.x, -pathOffset.y] + ); + } + return path.map(function(pathSegment) { + var newSegment = pathSegment.slice(0), point = {}; + for (var i = 1; i < pathSegment.length - 1; i += 2) { + point.x = pathSegment[i]; + point.y = pathSegment[i + 1]; + point = fabric.util.transformPoint(point, transform); + newSegment[i] = point.x; + newSegment[i + 1] = point.y; + } + return newSegment; + }); + } + + /** + * Calculate bounding box of a elliptic-arc + * @deprecated + * @param {Number} fx start point of arc + * @param {Number} fy + * @param {Number} rx horizontal radius + * @param {Number} ry vertical radius + * @param {Number} rot angle of horizontal axis + * @param {Number} large 1 or 0, whatever the arc is the big or the small on the 2 points + * @param {Number} sweep 1 or 0, 1 clockwise or counterclockwise direction + * @param {Number} tx end point of arc + * @param {Number} ty + */ + function getBoundsOfArc(fx, fy, rx, ry, rot, large, sweep, tx, ty) { + + var fromX = 0, fromY = 0, bound, bounds = [], + segs = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segs.length; i < len; i++) { + bound = getBoundsOfCurve(fromX, fromY, segs[i][1], segs[i][2], segs[i][3], segs[i][4], segs[i][5], segs[i][6]); + bounds.push({ x: bound[0].x + fx, y: bound[0].y + fy }); + bounds.push({ x: bound[1].x + fx, y: bound[1].y + fy }); + fromX = segs[i][5]; + fromY = segs[i][6]; + } + return bounds; + }; + + /** + * Draws arc + * @deprecated + * @param {CanvasRenderingContext2D} ctx + * @param {Number} fx + * @param {Number} fy + * @param {Array} coords coords of the arc, without the front 'A/a' + */ + function drawArc(ctx, fx, fy, coords) { + coords = coords.slice(0).unshift('X'); // command A or a does not matter + var beziers = fromArcToBeziers(fx, fy, coords); + beziers.forEach(function(bezier) { + ctx.bezierCurveTo.apply(ctx, bezier.slice(1)); + }); + }; + + /** + * Join path commands to go back to svg format + * @param {Array} pathData fabricJS parsed path commands + * @return {String} joined path 'M 0 0 L 20 30' + */ + fabric.util.joinPath = function(pathData) { + return pathData.map(function (segment) { return segment.join(' '); }).join(' '); + }; + fabric.util.parsePath = parsePath; + fabric.util.makePathSimpler = makePathSimpler; + fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints; + fabric.util.getPathSegmentsInfo = getPathSegmentsInfo; + fabric.util.getBoundsOfCurve = getBoundsOfCurve; + fabric.util.getPointOnPath = getPointOnPath; + fabric.util.transformPath = transformPath; + /** + * Typo of `fromArcToBeziers` kept for not breaking the api once corrected. + * Will be removed in fabric 5.0 + * @deprecated + */ + fabric.util.fromArcToBeizers = fromArcToBeziers; + // kept because we do not want to make breaking changes. + // but useless and deprecated. + fabric.util.getBoundsOfArc = getBoundsOfArc; + fabric.util.drawArc = drawArc; +})(); + + +(function() { + + var slice = Array.prototype.slice; + + /** + * Invokes method on all items in a given array + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + * @return {Array} + */ + function invoke(array, method) { + var args = slice.call(arguments, 2), result = []; + for (var i = 0, len = array.length; i < len; i++) { + result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); + } + return result; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function max(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function fill(array, value) { + var k = array.length; + while (k--) { + array[k] = value; + } + return array; + } + + /** + * @private + */ + function find(array, byProperty, condition) { + if (!array || array.length === 0) { + return; + } + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + if (byProperty) { + while (i--) { + if (condition(array[i][byProperty], result)) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (condition(array[i], result)) { + result = array[i]; + } + } + } + return result; + } + + /** + * @namespace fabric.util.array + */ + fabric.util.array = { + fill: fill, + invoke: invoke, + min: min, + max: max + }; + +})(); + + +(function() { + /** + * Copies all enumerable properties of one js object to another + * this does not and cannot compete with generic utils. + * Does not clone or extend fabric.Object subclasses. + * This is mostly for internal use and has extra handling for fabricJS objects + * it skips the canvas and group properties in deep cloning. + * @memberOf fabric.util.object + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + * @param {Boolean} [deep] Whether to extend nested objects + * @return {Object} + */ + + function extend(destination, source, deep) { + // JScript DontEnum bug is not taken care of + // the deep clone is for internal use, is not meant to avoid + // javascript traps or cloning html element or self referenced objects. + if (deep) { + if (!fabric.isLikelyNode && source instanceof Element) { + // avoid cloning deep images, canvases, + destination = source; + } + else if (source instanceof Array) { + destination = []; + for (var i = 0, len = source.length; i < len; i++) { + destination[i] = extend({ }, source[i], deep); + } + } + else if (source && typeof source === 'object') { + for (var property in source) { + if (property === 'canvas' || property === 'group') { + // we do not want to clone this props at all. + // we want to keep the keys in the copy + destination[property] = null; + } + else if (source.hasOwnProperty(property)) { + destination[property] = extend({ }, source[property], deep); + } + } + } + else { + // this sounds odd for an extend but is ok for recursive use + destination = source; + } + } + else { + for (var property in source) { + destination[property] = source[property]; + } + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * @memberOf fabric.util.object + * @param {Object} object Object to clone + * @param {Boolean} [deep] Whether to clone nested objects + * @return {Object} + */ + + //TODO: this function return an empty object if you try to clone null + function clone(object, deep) { + return extend({ }, object, deep); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + fabric.util.object.extend(fabric.util, fabric.Observable); +})(); + + +(function() { + + /** + * Camelizes a string + * @memberOf fabric.util.string + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ + function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); + } + + /** + * Capitalizes a string + * @memberOf fabric.util.string + * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. + * @return {String} Capitalized version of a string + */ + function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); + } + + /** + * Escapes XML in a string + * @memberOf fabric.util.string + * @param {String} string String to escape + * @return {String} Escaped version of a string + */ + function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + /** + * Divide a string in the user perceived single units + * @memberOf fabric.util.string + * @param {String} textstring String to escape + * @return {Array} array containing the graphemes + */ + function graphemeSplit(textstring) { + var i = 0, chr, graphemes = []; + for (i = 0, chr; i < textstring.length; i++) { + if ((chr = getWholeChar(textstring, i)) === false) { + continue; + } + graphemes.push(chr); + } + return graphemes; + } + + // taken from mdn in the charAt doc page. + function getWholeChar(str, i) { + var code = str.charCodeAt(i); + + if (isNaN(code)) { + return ''; // Position not found + } + if (code < 0xD800 || code > 0xDFFF) { + return str.charAt(i); + } + + // High surrogate (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 <= code && code <= 0xDBFF) { + if (str.length <= (i + 1)) { + throw 'High surrogate without following low surrogate'; + } + var next = str.charCodeAt(i + 1); + if (0xDC00 > next || next > 0xDFFF) { + throw 'High surrogate without following low surrogate'; + } + return str.charAt(i) + str.charAt(i + 1); + } + // Low surrogate (0xDC00 <= code && code <= 0xDFFF) + if (i === 0) { + throw 'Low surrogate without preceding high surrogate'; + } + var prev = str.charCodeAt(i - 1); + + // (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 > prev || prev > 0xDBFF) { + throw 'Low surrogate without preceding high surrogate'; + } + // We can pass over low surrogates now as the second component + // in a pair which we have already processed + return false; + } + + + /** + * String utilities + * @namespace fabric.util.string + */ + fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml, + graphemeSplit: graphemeSplit + }; +})(); + + +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }, + + IS_DONTENUM_BUGGY = (function() { + for (var p in { toString: 1 }) { + if (p === 'toString') { + return false; + } + } + return true; + })(), + + /** @ignore */ + addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + function callSuper(methodName) { + var parentMethod = null, + _this = this; + + // climb prototype chain to find method not equal to callee's method + while (_this.constructor.superclass) { + var superClassMethod = _this.constructor.superclass.prototype[methodName]; + if (_this[methodName] !== superClassMethod) { + parentMethod = superClassMethod; + break; + } + // eslint-disable-next-line + _this = _this.constructor.superclass.prototype; + } + + if (!parentMethod) { + return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); + } + + return (arguments.length > 1) + ? parentMethod.apply(this, slice.call(arguments, 1)) + : parentMethod.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param {Function} [parent] optional "Class" to inherit from + * @param {Object} [properties] Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; + } + + fabric.util.createClass = createClass; +})(); + + +(function () { + // since ie11 can use addEventListener but they do not support options, i need to check + var couldUseAttachEvent = !!fabric.document.createElement('div').attachEvent, + touchEvents = ['touchstart', 'touchmove', 'touchend']; + /** + * Adds an event listener to an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = function(element, eventName, handler, options) { + element && element.addEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + /** + * Removes an event listener from an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = function(element, eventName, handler, options) { + element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + function getTouchInfo(event) { + var touchProp = event.changedTouches; + if (touchProp && touchProp[0]) { + return touchProp[0]; + } + return event; + } + + fabric.util.getPointer = function(event) { + var element = event.target, + scroll = fabric.util.getScrollLeftTop(element), + _evt = getTouchInfo(event); + return { + x: _evt.clientX + scroll.left, + y: _evt.clientY + scroll.top + }; + }; + + fabric.util.isTouchEvent = function(event) { + return touchEvents.indexOf(event.type) > -1 || event.pointerType === 'touch'; + }; +})(); + + +(function () { + + /** + * Cross-browser wrapper for setting element's style + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {Object} styles + * @return {HTMLElement} Element that was passed as a first argument + */ + function setStyle(element, styles) { + var elementStyle = element.style; + if (!elementStyle) { + return element; + } + if (typeof styles === 'string') { + element.style.cssText += ';' + styles; + return styles.indexOf('opacity') > -1 + ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) + : element; + } + for (var property in styles) { + if (property === 'opacity') { + setOpacity(element, styles[property]); + } + else { + var normalizedProperty = (property === 'float' || property === 'cssFloat') + ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') + : property; + elementStyle[normalizedProperty] = styles[property]; + } + } + return element; + } + + var parseEl = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, + + /** @ignore */ + setOpacity = function (element) { return element; }; + + if (supportsOpacity) { + /** @ignore */ + setOpacity = function(element, value) { + element.style.opacity = value; + return element; + }; + } + else if (supportsFilters) { + /** @ignore */ + setOpacity = function(element, value) { + var es = element.style; + if (element.currentStyle && !element.currentStyle.hasLayout) { + es.zoom = 1; + } + if (reOpacity.test(es.filter)) { + value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')'); + es.filter = es.filter.replace(reOpacity, value); + } + else { + es.filter += ' alpha(opacity=' + (value * 100) + ')'; + } + return element; + }; + } + + fabric.util.setStyle = setStyle; + +})(); + + +(function() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + var sliceCanConvertNodelists, + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch (err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + + /** + * Adds class to an element + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if (element && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + function wrapElement(element, wrapper, attributes) { + if (typeof wrapper === 'string') { + wrapper = makeElement(wrapper, attributes); + } + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); + } + wrapper.appendChild(element); + return wrapper; + } + + /** + * Returns element scroll offsets + * @memberOf fabric.util + * @param {HTMLElement} element Element to operate on + * @return {Object} Object with left/top values + */ + function getScrollLeftTop(element) { + + var left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + // While loop checks (and then sets element to) .parentNode OR .host + // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, + // but the .parentNode of a root ShadowDOM node will always be null, instead + // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 + while (element && (element.parentNode || element.host)) { + + // Set element to element parent, or 'host' in case of ShadowDOM + element = element.parentNode || element.host; + + if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + + if (element.nodeType === 1 && element.style.position === 'fixed') { + break; + } + } + + return { left: left, top: top }; + } + + /** + * Returns offset for a given element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + var docElem, + doc = element && element.ownerDocument, + box = { left: 0, top: 0 }, + offset = { left: 0, top: 0 }, + scrollLeftTop, + offsetAttributes = { + borderLeftWidth: 'left', + borderTopWidth: 'top', + paddingLeft: 'left', + paddingTop: 'top' + }; + + if (!doc) { + return offset; + } + + for (var attr in offsetAttributes) { + offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; + } + + docElem = doc.documentElement; + if ( typeof element.getBoundingClientRect !== 'undefined' ) { + box = element.getBoundingClientRect(); + } + + scrollLeftTop = getScrollLeftTop(element); + + return { + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top + }; + } + + /** + * Returns style attribute value of a given element + * @memberOf fabric.util + * @param {HTMLElement} element Element to get style attribute for + * @param {String} attr Style attribute to get for element + * @return {String} Style attribute value of the given element. + */ + var getElementStyle; + if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { + getElementStyle = function(element, attr) { + var style = fabric.document.defaultView.getComputedStyle(element, null); + return style ? style[attr] : undefined; + }; + } + else { + getElementStyle = function(element, attr) { + var value = element.style[attr]; + if (!value && element.currentStyle) { + value = element.currentStyle[attr]; + } + return value; + }; + } + + (function () { + var style = fabric.document.documentElement.style, + selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = fabric.util.falseFunction; + } + if (selectProp) { + element.style[selectProp] = 'none'; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = 'on'; + } + return element; + } + + /** + * Makes element selectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + function getNodeCanvas(element) { + var impl = fabric.jsdomImplForWrapper(element); + return impl._canvas || impl._image; + }; + + function cleanUpJsdomNode(element) { + if (!fabric.isLikelyNode) { + return; + } + var impl = fabric.jsdomImplForWrapper(element); + if (impl) { + impl._image = null; + impl._canvas = null; + // unsure if necessary + impl._currentSrc = null; + impl._attributes = null; + impl._classList = null; + } + } + + function setImageSmoothing(ctx, value) { + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled + || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; + ctx.imageSmoothingEnabled = value; + } + + /** + * setImageSmoothing sets the context imageSmoothingEnabled property. + * Used by canvas and by ImageObject. + * @memberOf fabric.util + * @since 4.0.0 + * @param {HTMLRenderingContext2D} ctx to set on + * @param {Boolean} value true or false + */ + fabric.util.setImageSmoothing = setImageSmoothing; + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.addClass = addClass; + fabric.util.makeElement = makeElement; + fabric.util.wrapElement = wrapElement; + fabric.util.getScrollLeftTop = getScrollLeftTop; + fabric.util.getElementOffset = getElementOffset; + fabric.util.getNodeCanvas = getNodeCanvas; + fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; + +})(); + + +(function() { + + function addParamToUrl(url, param) { + return url + (/\?/.test(url) ? '&' : '?') + param; + } + + function emptyFn() { } + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {String} [options.parameters] parameters to append to url in GET or in body + * @param {String} [options.body] body to send with POST or PUT request + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = new fabric.window.XMLHttpRequest(), + body = options.body || options.parameters; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); + + +/** + * Wrapper around `console.log` (when available) + * @param {*} [values] Values to log + */ +fabric.log = console.log; + +/** + * Wrapper around `console.warn` (when available) + * @param {*} [values] Values to log as a warning + */ +fabric.warn = console.warn; + + +(function() { + + function noop() { + return false; + } + + function defaultEasing(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change (in ms) + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ + function animate(options) { + var cancel = false; + requestAnimFrame(function(timestamp) { + options || (options = { }); + + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || noop, + abort = options.abort || noop, + onComplete = options.onComplete || noop, + easing = options.easing || defaultEasing, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick(ticktime) { + // TODO: move abort call after calculation + // and pass (current,valuePerc, timePerc) as arguments + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start), + timePerc = currentTime / duration, + current = easing(currentTime, startValue, byValue, duration), + valuePerc = Math.abs((current - startValue) / byValue); + if (cancel) { + return; + } + if (abort(current, valuePerc, timePerc)) { + // remove this in 4.0 + // does to even make sense to abort and run onComplete? + onComplete(endValue, 1, 1); + return; + } + if (time > finish) { + onChange(endValue, 1, 1); + onComplete(endValue, 1, 1); + return; + } + else { + onChange(current, valuePerc, timePerc); + requestAnimFrame(tick); + } + })(start); + }); + return function() { + cancel = true; + }; + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback) { + return fabric.window.setTimeout(callback, 1000 / 60); + }; + + var _cancelAnimFrame = fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; + + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + function requestAnimFrame() { + return _requestAnimFrame.apply(fabric.window, arguments); + } + + function cancelAnimFrame() { + return _cancelAnimFrame.apply(fabric.window, arguments); + } + + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.cancelAnimFrame = cancelAnimFrame; +})(); + + +(function() { + // Calculate an in-between color. Returns a "rgba()" string. + // Credit: Edwin Martin + // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js + function calculateColor(begin, end, pos) { + var color = 'rgba(' + + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' + + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' + + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); + + color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); + color += ')'; + return color; + } + + /** + * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {String} fromColor The starting color in hex or rgb(a) format. + * @param {String} toColor The starting color in hex or rgb(a) format. + * @param {Number} [duration] Duration of change (in ms). + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ + function animateColor(fromColor, toColor, duration, options) { + var startColor = new fabric.Color(fromColor).getSource(), + endColor = new fabric.Color(toColor).getSource(), + originalOnComplete = options.onComplete, + originalOnChange = options.onChange; + options = options || {}; + + return fabric.util.animate(fabric.util.object.extend(options, { + duration: duration || 500, + startValue: startColor, + endValue: endColor, + byValue: endColor, + easing: function (currentTime, startValue, byValue, duration) { + var posValue = options.colorEasing + ? options.colorEasing(currentTime, duration) + : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); + return calculateColor(startValue, byValue, posValue); + }, + // has to take in account for color restoring; + onComplete: function(current, valuePerc, timePerc) { + if (originalOnComplete) { + return originalOnComplete( + calculateColor(endColor, endColor, 0), + valuePerc, + timePerc + ); + } + }, + onChange: function(current, valuePerc, timePerc) { + if (originalOnChange) { + if (Array.isArray(current)) { + return originalOnChange( + calculateColor(current, current, 0), + valuePerc, + timePerc + ); + } + originalOnChange(current, valuePerc, timePerc); + } + } + })); + } + + fabric.util.animateColor = animateColor; + +})(); + + +(function() { + + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + //handle the 0/0 case: + if (c === 0 && a === 0) { + s = p / (2 * Math.PI) * Math.asin(1); + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + } + return { a: a, c: c, p: p, s: s }; + } + + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); + } + + /** + * Cubic easing out + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c * ((t = t / d - 1) * t * t + 1) + b; + } + + /** + * Cubic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t + 2) + b; + } + + /** + * Quartic easing in + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c * (t /= d) * t * t * t + b; + } + + /** + * Quartic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t = t / d - 1) * t * t * t - 1) + b; + } + + /** + * Quartic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t + b; + } + return -c / 2 * ((t -= 2) * t * t * t - 2) + b; + } + + /** + * Quintic easing in + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + } + + /** + * Quintic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + } + + /** + * Quintic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + } + + /** + * Sinusoidal easing in + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Sinusoidal easing out + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t / d * (Math.PI / 2)) + b; + } + + /** + * Sinusoidal easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + } + + /** + * Exponential easing in + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + } + + /** + * Exponential easing out + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + } + + /** + * Exponential easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t === 0) { + return b; + } + if (t === d) { + return b + c; + } + t /= d / 2; + if (t < 1) { + return c / 2 * Math.pow(2, 10 * (t - 1)) + b; + } + return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * Circular easing in + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + } + + /** + * Circular easing out + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + } + + /** + * Circular easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; + } + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + } + + /** + * Elastic easing in + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; + } + + /** + * Elastic easing out + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; + } + + /** + * Elastic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d / 2; + if (t === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + var opts = normalize(a, c, p, s); + if (t < 1) { + return -0.5 * elastic(opts, t, d) + b; + } + return opts.a * Math.pow(2, -10 * (t -= 1)) * + Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.c + b; + } + + /** + * Backwards easing in + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * (t /= d) * t * ((s + 1) * t - s) + b; + } + + /** + * Backwards easing out + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + } + + /** + * Backwards easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + t /= d / 2; + if (t < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + } + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + } + + /** + * Bouncing easing in + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d - t, 0, c, d) + b; + } + + /** + * Bouncing easing out + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } + else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; + } + else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; + } + } + + /** + * Bouncing easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d / 2) { + return easeInBounce (t * 2, 0, c, d) * 0.5 + b; + } + return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + + /** + * Easing functions + * See Easing Equations by Robert Penner + * @namespace fabric.util.ease + */ + fabric.util.ease = { + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c * (t /= d) * t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d / 2); + if (t < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c * (t /= d) * t * t + b; + }, + + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +})(); + + +(function(global) { + + 'use strict'; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + parseUnit = fabric.util.parseUnit, + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + + svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line', + 'image', 'text'], + svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'], + svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'], + svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'], + + attributesMap = { + cx: 'left', + x: 'left', + r: 'radius', + cy: 'top', + y: 'top', + display: 'visible', + visibility: 'visible', + transform: 'transformMatrix', + 'fill-opacity': 'fillOpacity', + 'fill-rule': 'fillRule', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-style': 'fontStyle', + 'font-weight': 'fontWeight', + 'letter-spacing': 'charSpacing', + 'paint-order': 'paintFirst', + 'stroke-dasharray': 'strokeDashArray', + 'stroke-dashoffset': 'strokeDashOffset', + 'stroke-linecap': 'strokeLineCap', + 'stroke-linejoin': 'strokeLineJoin', + 'stroke-miterlimit': 'strokeMiterLimit', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'text-decoration': 'textDecoration', + 'text-anchor': 'textAnchor', + opacity: 'opacity', + 'clip-path': 'clipPath', + 'clip-rule': 'clipRule', + 'vector-effect': 'strokeUniform', + 'image-rendering': 'imageSmoothing', + }, + + colorAttributes = { + stroke: 'strokeOpacity', + fill: 'fillOpacity' + }, + + fSize = 'font-size', cPath = 'clip-path'; + + fabric.svgValidTagNamesRegEx = getSvgRegex(svgValidTagNames); + fabric.svgViewBoxElementsRegEx = getSvgRegex(svgViewBoxElements); + fabric.svgInvalidAncestorsRegEx = getSvgRegex(svgInvalidAncestors); + fabric.svgValidParentsRegEx = getSvgRegex(svgValidParents); + + fabric.cssRules = { }; + fabric.gradientDefs = { }; + fabric.clipPaths = { }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + function normalizeValue(attr, value, parentAttributes, fontSize) { + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; + + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + else if (attr === 'strokeUniform') { + return (value === 'non-scaling-stroke'); + } + else if (attr === 'strokeDashArray') { + if (value === 'none') { + value = null; + } + else { + value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); + } + } + else if (attr === 'transformMatrix') { + if (parentAttributes && parentAttributes.transformMatrix) { + value = multiplyTransformMatrices( + parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); + } + else { + value = fabric.parseTransformAttribute(value); + } + } + else if (attr === 'visible') { + value = value !== 'none' && value !== 'hidden'; + // display=none on parent element always takes precedence over child element + if (parentAttributes && parentAttributes.visible === false) { + value = false; + } + } + else if (attr === 'opacity') { + value = parseFloat(value); + if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { + value *= parentAttributes.opacity; + } + } + else if (attr === 'textAnchor' /* text-anchor */) { + value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; + } + else if (attr === 'charSpacing') { + // parseUnit returns px and we convert it to em + parsed = parseUnit(value, fontSize) / fontSize * 1000; + } + else if (attr === 'paintFirst') { + var fillIndex = value.indexOf('fill'); + var strokeIndex = value.indexOf('stroke'); + var value = 'fill'; + if (fillIndex > -1 && strokeIndex > -1 && strokeIndex < fillIndex) { + value = 'stroke'; + } + else if (fillIndex === -1 && strokeIndex > -1) { + value = 'stroke'; + } + } + else if (attr === 'href' || attr === 'xlink:href' || attr === 'font') { + return value; + } + else if (attr === 'imageSmoothing') { + return (value === 'optimizeQuality'); + } + else { + parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); + } + + return (!isArray && isNaN(parsed) ? value : parsed); + } + + /** + * @private + */ + function getSvgRegex(arr) { + return new RegExp('^(' + arr.join('|') + ')\\b', 'i'); + } + + /** + * @private + * @param {Object} attributes Array of attributes to parse + */ + function _setStrokeFillOpacity(attributes) { + for (var attr in colorAttributes) { + + if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { + continue; + } + + if (typeof attributes[attr] === 'undefined') { + if (!fabric.Object.prototype[attr]) { + continue; + } + attributes[attr] = fabric.Object.prototype[attr]; + } + + if (attributes[attr].indexOf('url(') === 0) { + continue; + } + + var color = new fabric.Color(attributes[attr]); + attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); + } + return attributes; + } + + /** + * @private + */ + function _getMultipleNodes(doc, nodeNames) { + var nodeName, nodeArray = [], nodeList, i, len; + for (i = 0, len = nodeNames.length; i < len; i++) { + nodeName = nodeNames[i]; + nodeList = doc.getElementsByTagName(nodeName); + nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); + } + return nodeArray; + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix + */ + fabric.parseTransformAttribute = (function() { + function rotateMatrix(matrix, args) { + var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), + x = 0, y = 0; + if (args.length === 3) { + x = args[1]; + y = args[2]; + } + + matrix[0] = cos; + matrix[1] = sin; + matrix[2] = -sin; + matrix[3] = cos; + matrix[4] = x - (cos * x - sin * y); + matrix[5] = y - (sin * x + cos * y); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewMatrix(matrix, args, pos) { + matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); + } + + function translateMatrix(matrix, args) { + matrix[4] = args[0]; + if (args.length === 2) { + matrix[5] = args[1]; + } + } + + // identity matrix + var iMatrix = fabric.iMatrix, + + // == begin transform regexp + number = fabric.reNum, + + commaWsp = fabric.commaWsp, + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + ')' + + commaWsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', + + transformList = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transformList), + // == end transform regexp + + reTransform = new RegExp(transform, 'g'); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(), + matrices = []; + + // return if no argument was given or + // an argument does not match transform attribute regexp + if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { + return matrix; + } + + attributeValue.replace(reTransform, function(match) { + + var m = new RegExp(transform).exec(match).filter(function (match) { + // match !== '' && match != null + return (!!match); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch (operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + args[0] = fabric.util.degreesToRadians(args[0]); + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewMatrix(matrix, args, 2); + break; + case 'skewY': + skewMatrix(matrix, args, 1); + break; + case 'matrix': + matrix = args; + break; + } + + // snapshot current matrix into matrices array + matrices.push(matrix.concat()); + // reset + matrix = iMatrix.concat(); + }); + + var combinedMatrix = matrices[0]; + while (matrices.length > 1) { + matrices.shift(); + combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); + } + return combinedMatrix; + }; + })(); + + /** + * @private + */ + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); + + attr = pair[0].trim().toLowerCase(); + value = pair[1].trim(); + + oStyle[attr] = value; + }); + } + + /** + * @private + */ + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') { + continue; + } + + attr = prop.toLowerCase(); + value = style[prop]; + + oStyle[attr] = value; + } + } + + /** + * @private + */ + function getGlobalStylesForElement(element, svgUid) { + var styles = { }; + for (var rule in fabric.cssRules[svgUid]) { + if (elementMatchesRule(element, rule.split(' '))) { + for (var property in fabric.cssRules[svgUid][rule]) { + styles[property] = fabric.cssRules[svgUid][rule][property]; + } + } + } + return styles; + } + + /** + * @private + */ + function elementMatchesRule(element, selectors) { + var firstMatching, parentMatching = true; + //start from rightmost selector. + firstMatching = selectorMatches(element, selectors.pop()); + if (firstMatching && selectors.length) { + parentMatching = doesSomeParentMatch(element, selectors); + } + return firstMatching && parentMatching && (selectors.length === 0); + } + + function doesSomeParentMatch(element, selectors) { + var selector, parentMatching = true; + while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { + if (parentMatching) { + selector = selectors.pop(); + } + element = element.parentNode; + parentMatching = selectorMatches(element, selector); + } + return selectors.length === 0; + } + + /** + * @private + */ + function selectorMatches(element, selector) { + var nodeName = element.nodeName, + classNames = element.getAttribute('class'), + id = element.getAttribute('id'), matcher, i; + // i check if a selector matches slicing away part from it. + // if i get empty string i should match + matcher = new RegExp('^' + nodeName, 'i'); + selector = selector.replace(matcher, ''); + if (id && selector.length) { + matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + if (classNames && selector.length) { + classNames = classNames.split(' '); + for (i = classNames.length; i--;) { + matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + } + return selector.length === 0; + } + + /** + * @private + * to support IE8 missing getElementById on SVGdocument and on node xmlDOM + */ + function elementById(doc, id) { + var el; + doc.getElementById && (el = doc.getElementById(id)); + if (el) { + return el; + } + var node, i, len, nodelist = doc.getElementsByTagName('*'); + for (i = 0, len = nodelist.length; i < len; i++) { + node = nodelist[i]; + if (id === node.getAttribute('id')) { + return node; + } + } + } + + /** + * @private + */ + function parseUseDirectives(doc) { + var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; + while (nodelist.length && i < nodelist.length) { + var el = nodelist[i], + xlinkAttribute = el.getAttribute('xlink:href') || el.getAttribute('href'); + + if (xlinkAttribute === null) { + return; + } + + var xlink = xlinkAttribute.substr(1), + x = el.getAttribute('x') || 0, + y = el.getAttribute('y') || 0, + el2 = elementById(doc, xlink).cloneNode(true), + currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', + parentNode, + oldLength = nodelist.length, attr, + j, + attrs, + len, + namespace = fabric.svgNS; + + applyViewboxTransform(el2); + if (/^svg$/i.test(el2.nodeName)) { + var el3 = el2.ownerDocument.createElementNS(namespace, 'g'); + for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + el3.setAttributeNS(namespace, attr.nodeName, attr.nodeValue); + } + // el2.firstChild != null + while (el2.firstChild) { + el3.appendChild(el2.firstChild); + } + el2 = el3; + } + + for (j = 0, attrs = el.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + if (attr.nodeName === 'x' || attr.nodeName === 'y' || + attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { + continue; + } + + if (attr.nodeName === 'transform') { + currentTrans = attr.nodeValue + ' ' + currentTrans; + } + else { + el2.setAttribute(attr.nodeName, attr.nodeValue); + } + } + + el2.setAttribute('transform', currentTrans); + el2.setAttribute('instantiated_by_use', '1'); + el2.removeAttribute('id'); + parentNode = el.parentNode; + parentNode.replaceChild(el2, el); + // some browsers do not shorten nodelist after replaceChild (IE8) + if (nodelist.length === oldLength) { + i++; + } + } + } + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // matches, e.g.: +14.56e-12, etc. + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*' + + '$' + ); + + /** + * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements + */ + function applyViewboxTransform(element) { + if (!fabric.svgViewBoxElementsRegEx.test(element.nodeName)) { + return {}; + } + var viewBoxAttr = element.getAttribute('viewBox'), + scaleX = 1, + scaleY = 1, + minX = 0, + minY = 0, + viewBoxWidth, viewBoxHeight, matrix, el, + widthAttr = element.getAttribute('width'), + heightAttr = element.getAttribute('height'), + x = element.getAttribute('x') || 0, + y = element.getAttribute('y') || 0, + preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', + missingViewBox = (!viewBoxAttr || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), + missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), + toBeParsed = missingViewBox && missingDimAttr, + parsedDim = { }, translateMatrix = '', widthDiff = 0, heightDiff = 0; + + parsedDim.width = 0; + parsedDim.height = 0; + parsedDim.toBeParsed = toBeParsed; + + if (missingViewBox) { + if (((x || y) && element.parentNode && element.parentNode.nodeName !== '#document')) { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + matrix = (element.getAttribute('transform') || '') + translateMatrix; + element.setAttribute('transform', matrix); + element.removeAttribute('x'); + element.removeAttribute('y'); + } + } + + if (toBeParsed) { + return parsedDim; + } + + if (missingViewBox) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + // set a transform for elements that have x y and are inner(only) SVGs + return parsedDim; + } + minX = -parseFloat(viewBoxAttr[1]); + minY = -parseFloat(viewBoxAttr[2]); + viewBoxWidth = parseFloat(viewBoxAttr[3]); + viewBoxHeight = parseFloat(viewBoxAttr[4]); + parsedDim.minX = minX; + parsedDim.minY = minY; + parsedDim.viewBoxWidth = viewBoxWidth; + parsedDim.viewBoxHeight = viewBoxHeight; + if (!missingDimAttr) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + scaleX = parsedDim.width / viewBoxWidth; + scaleY = parsedDim.height / viewBoxHeight; + } + else { + parsedDim.width = viewBoxWidth; + parsedDim.height = viewBoxHeight; + } + + // default is to preserve aspect ratio + preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); + if (preserveAspectRatio.alignX !== 'none') { + //translate all container for the effect of Mid, Min, Max + if (preserveAspectRatio.meetOrSlice === 'meet') { + scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); + // calculate additional translation to move the viewbox + } + if (preserveAspectRatio.meetOrSlice === 'slice') { + scaleY = scaleX = (scaleX > scaleY ? scaleX : scaleY); + // calculate additional translation to move the viewbox + } + widthDiff = parsedDim.width - viewBoxWidth * scaleX; + heightDiff = parsedDim.height - viewBoxHeight * scaleX; + if (preserveAspectRatio.alignX === 'Mid') { + widthDiff /= 2; + } + if (preserveAspectRatio.alignY === 'Mid') { + heightDiff /= 2; + } + if (preserveAspectRatio.alignX === 'Min') { + widthDiff = 0; + } + if (preserveAspectRatio.alignY === 'Min') { + heightDiff = 0; + } + } + + if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { + return parsedDim; + } + if ((x || y) && element.parentNode.nodeName !== '#document') { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + } + + matrix = translateMatrix + ' matrix(' + scaleX + + ' 0' + + ' 0 ' + + scaleY + ' ' + + (minX * scaleX + widthDiff) + ' ' + + (minY * scaleY + heightDiff) + ') '; + // seems unused. + // parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); + if (element.nodeName === 'svg') { + el = element.ownerDocument.createElementNS(fabric.svgNS, 'g'); + // element.firstChild != null + while (element.firstChild) { + el.appendChild(element.firstChild); + } + element.appendChild(el); + } + else { + el = element; + el.removeAttribute('x'); + el.removeAttribute('y'); + matrix = el.getAttribute('transform') + matrix; + } + el.setAttribute('transform', matrix); + return parsedDim; + } + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) + && !element.getAttribute('instantiated_by_use')) { + return true; + } + } + return false; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; + * It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [parsingOptions] options for parsing document + * @param {String} [parsingOptions.crossOrigin] crossOrigin settings + */ + fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { + if (!doc) { + return; + } + + parseUseDirectives(doc); + + var svgUid = fabric.Object.__uid++, i, len, + options = applyViewboxTransform(doc), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; + options.svgUid = svgUid; + + if (descendants.length === 0 && fabric.isLikelyNode) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes('//*[name(.)!="svg"]'); + var arr = []; + for (i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + applyViewboxTransform(el); + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')) && + !hasAncestorWithNodeName(el, fabric.svgInvalidAncestorsRegEx); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + if (!elements || (elements && !elements.length)) { + callback && callback([], {}); + return; + } + var clipPaths = { }; + descendants.filter(function(el) { + return el.nodeName.replace('svg:', '') === 'clipPath'; + }).forEach(function(el) { + var id = el.getAttribute('id'); + clipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) { + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); + }); + }); + fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + fabric.clipPaths[svgUid] = clipPaths; + // Precedence of rules: style > class > attribute + fabric.parseElements(elements, function(instances, elements) { + if (callback) { + callback(instances, options, elements, descendants); + delete fabric.gradientDefs[svgUid]; + delete fabric.cssRules[svgUid]; + delete fabric.clipPaths[svgUid]; + } + }, clone(options), reviver, parsingOptions); + }; + + function recursivelyParseGradientsXlink(doc, gradient) { + var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], + xlinkAttr = 'xlink:href', + xLink = gradient.getAttribute(xlinkAttr).substr(1), + referencedGradient = elementById(doc, xLink); + if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { + recursivelyParseGradientsXlink(doc, referencedGradient); + } + gradientsAttrs.forEach(function(attr) { + if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { + gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); + } + }); + if (!gradient.children.length) { + var referenceClone = referencedGradient.cloneNode(true); + while (referenceClone.firstChild) { + gradient.appendChild(referenceClone.firstChild); + } + } + gradient.removeAttribute(xlinkAttr); + } + + var reFontDeclaration = new RegExp( + '(normal|italic)?\\s*(normal|small-caps)?\\s*' + + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + + fabric.reNum + + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); + + extend(fabric, { + /** + * Parses a short font declaration, building adding its properties to a style object + * @static + * @function + * @memberOf fabric + * @param {String} value font declaration + * @param {Object} oStyle definition + */ + parseFontDeclaration: function(value, oStyle) { + var match = value.match(reFontDeclaration); + + if (!match) { + return; + } + var fontStyle = match[1], + // font variant is not used + // fontVariant = match[2], + fontWeight = match[3], + fontSize = match[4], + lineHeight = match[5], + fontFamily = match[6]; + + if (fontStyle) { + oStyle.fontStyle = fontStyle; + } + if (fontWeight) { + oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); + } + if (fontSize) { + oStyle.fontSize = parseUnit(fontSize); + } + if (fontFamily) { + oStyle.fontFamily = fontFamily; + } + if (lineHeight) { + oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var tagArray = [ + 'linearGradient', + 'radialGradient', + 'svg:linearGradient', + 'svg:radialGradient'], + elList = _getMultipleNodes(doc, tagArray), + el, j = 0, gradientDefs = { }; + j = elList.length; + while (j--) { + el = elList[j]; + if (el.getAttribute('xlink:href')) { + recursivelyParseGradientsXlink(doc, el); + } + gradientDefs[el.getAttribute('id')] = el; + } + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes, svgUid) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }, + fontSize, parentFontSize; + + if (typeof svgUid === 'undefined') { + svgUid = element.getAttribute('svgUid'); + } + // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards + if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { // eslint-disable-line + memo[attr] = value; + } + return memo; + }, { }); + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + var cssAttrs = extend( + getGlobalStylesForElement(element, svgUid), + fabric.parseStyleAttribute(element) + ); + ownAttributes = extend( + ownAttributes, + cssAttrs + ); + if (cssAttrs[cPath]) { + element.setAttribute(cPath, cssAttrs[cPath]); + } + fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; + if (ownAttributes[fSize]) { + // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. + ownAttributes[fSize] = fontSize = parseUnit(ownAttributes[fSize], parentFontSize); + } + + var normalizedAttr, normalizedValue, normalizedStyle = {}; + for (var attr in ownAttributes) { + normalizedAttr = normalizeAttr(attr); + normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); + normalizedStyle[normalizedAttr] = normalizedValue; + } + if (normalizedStyle && normalizedStyle.font) { + fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); + } + var mergedAttrs = extend(parentAttributes, normalizedStyle); + return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver, parsingOptions) { + new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) { + return oStyle; + } + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param {String} points points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) { + return null; + } + + // replace commas with whitespace and remove bookending whitespace + points = points.replace(/,/g, ' ').trim(); + + points = points.split(/\s+/); + var parsedPoints = [], i, len; + + for (i = 0, len = points.length; i < len; i += 2) { + parsedPoints.push({ + x: parseFloat(points[i]), + y: parseFloat(points[i + 1]) + }); + } + + // odd number of points is an error + // if (parsedPoints.length % 2 !== 0) { + // return null; + // } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), i, len, + allRules = { }, rules; + + // very crude parsing of style contents + for (i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[i].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + if (styleContents.trim() === '') { + continue; + } + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + // eslint-disable-next-line no-loop-func + rules.forEach(function(rule) { + + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), + ruleObj = { }, declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + for (i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + ruleObj[property] = value; + } + rule = match[1]; + rule.split(',').forEach(function(_rule) { + _rule = _rule.replace(/^svg/i, '').trim(); + if (_rule === '') { + return; + } + if (allRules[_rule]) { + fabric.util.object.extend(allRules[_rule], ruleObj); + } + else { + allRules[_rule] = fabric.util.object.clone(ruleObj); + } + }); + }); + } + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. + * Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberOf fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromURL: function(url, callback, reviver, options) { + + url = url.replace(/^\n\s*/, '').trim(); + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml || !xml.documentElement) { + callback && callback(null); + return false; + } + + fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { + callback && callback(results, _options, elements, allElements); + }, reviver, options); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberOf fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromString: function(string, callback, reviver, options) { + var parser = new fabric.window.DOMParser(), + doc = parser.parseFromString(string.trim(), 'text/xml'); + fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { + callback(results, _options, elements, allElements); + }, reviver, options); + } + }); + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions, doc) { + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + this.svgUid = (options && options.svgUid) || 0; + this.parsingOptions = parsingOptions; + this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g; + this.doc = doc; +}; + +(function(proto) { + proto.parse = function() { + this.instances = new Array(this.elements.length); + this.numElements = this.elements.length; + this.createObjects(); + }; + + proto.createObjects = function() { + var _this = this; + this.elements.forEach(function(element, i) { + element.setAttribute('svgUid', _this.svgUid); + _this.createObject(element, i); + }); + }; + + proto.findTag = function(el) { + return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; + }; + + proto.createObject = function(el, index) { + var klass = this.findTag(el); + if (klass && klass.fromElement) { + try { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + catch (err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }; + + proto.createCallback = function(index, el) { + var _this = this; + return function(obj) { + var _options; + _this.resolveGradient(obj, el, 'fill'); + _this.resolveGradient(obj, el, 'stroke'); + if (obj instanceof fabric.Image && obj._originalElement) { + _options = obj.parsePreserveAspectRatioAttribute(el); + } + obj._removeTransformMatrix(_options); + _this.resolveClipPath(obj, el); + _this.reviver && _this.reviver(el, obj); + _this.instances[index] = obj; + _this.checkIfDone(); + }; + }; + + proto.extractPropertyDefinition = function(obj, property, storage) { + var value = obj[property], regex = this.regexUrl; + if (!regex.test(value)) { + return; + } + regex.lastIndex = 0; + var id = regex.exec(value)[1]; + regex.lastIndex = 0; + return fabric[storage][this.svgUid][id]; + }; + + proto.resolveGradient = function(obj, el, property) { + var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); + if (gradientDef) { + var opacityAttr = el.getAttribute(property + '-opacity'); + var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); + obj.set(property, gradient); + } + }; + + proto.createClipPathCallback = function(obj, container) { + return function(_newObj) { + _newObj._removeTransformMatrix(); + _newObj.fillRule = _newObj.clipRule; + container.push(_newObj); + }; + }; + + proto.resolveClipPath = function(obj, usingElement) { + var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'), + element, klass, objTransformInv, container, gTransform, options; + if (clipPath) { + container = []; + objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix()); + // move the clipPath tag as sibling to the real element that is using it + var clipPathTag = clipPath[0].parentNode; + var clipPathOwner = usingElement; + while (clipPathOwner.parentNode && clipPathOwner.getAttribute('clip-path') !== obj.clipPath) { + clipPathOwner = clipPathOwner.parentNode; + } + clipPathOwner.parentNode.appendChild(clipPathTag); + for (var i = 0; i < clipPath.length; i++) { + element = clipPath[i]; + klass = this.findTag(element); + klass.fromElement( + element, + this.createClipPathCallback(obj, container), + this.options + ); + } + if (container.length === 1) { + clipPath = container[0]; + } + else { + clipPath = new fabric.Group(container); + } + gTransform = fabric.util.multiplyTransformMatrices( + objTransformInv, + clipPath.calcTransformMatrix() + ); + if (clipPath.clipPath) { + this.resolveClipPath(clipPath, clipPathOwner); + } + var options = fabric.util.qrDecompose(gTransform); + clipPath.flipX = false; + clipPath.flipY = false; + clipPath.set('scaleX', options.scaleX); + clipPath.set('scaleY', options.scaleY); + clipPath.angle = options.angle; + clipPath.skewX = options.skewX; + clipPath.skewY = 0; + clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center'); + obj.clipPath = clipPath; + } + else { + // if clip-path does not resolve to any element, delete the property. + delete obj.clipPath; + } + }; + + proto.checkIfDone = function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + // eslint-disable-next-line no-eq-null, eqeqeq + return el != null; + }); + this.callback(this.instances, this.elements); + } + }; +})(fabric.ElementsParser.prototype); + + +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Point) { + fabric.warn('fabric.Point is already defined'); + return; + } + + fabric.Point = Point; + + /** + * Point class + * @class fabric.Point + * @memberOf fabric + * @constructor + * @param {Number} x + * @param {Number} y + * @return {fabric.Point} thisArg + */ + function Point(x, y) { + this.x = x; + this.y = y; + } + + Point.prototype = /** @lends fabric.Point.prototype */ { + + type: 'point', + + constructor: Point, + + /** + * Adds another point to this one and returns another one + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * Adds another point to this one + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * Adds value to this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * Adds value to this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * Subtracts another point from this point and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * Subtracts another point from this point + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + subtractEquals: function (that) { + this.x -= that.x; + this.y -= that.y; + return this; + }, + + /** + * Subtracts value from this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + scalarSubtract: function (scalar) { + return new Point(this.x - scalar, this.y - scalar); + }, + + /** + * Subtracts value from this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarSubtractEquals: function (scalar) { + this.x -= scalar; + this.y -= scalar; + return this; + }, + + /** + * Multiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + multiply: function (scalar) { + return new Point(this.x * scalar, this.y * scalar); + }, + + /** + * Multiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + multiplyEquals: function (scalar) { + this.x *= scalar; + this.y *= scalar; + return this; + }, + + /** + * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + divide: function (scalar) { + return new Point(this.x / scalar, this.y / scalar); + }, + + /** + * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + divideEquals: function (scalar) { + this.x /= scalar; + this.y /= scalar; + return this; + }, + + /** + * Returns true if this point is equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + eq: function (that) { + return (this.x === that.x && this.y === that.y); + }, + + /** + * Returns true if this point is less than another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lt: function (that) { + return (this.x < that.x && this.y < that.y); + }, + + /** + * Returns true if this point is less than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lte: function (that) { + return (this.x <= that.x && this.y <= that.y); + }, + + /** + + * Returns true if this point is greater another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gt: function (that) { + return (this.x > that.x && this.y > that.y); + }, + + /** + * Returns true if this point is greater than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gte: function (that) { + return (this.x >= that.x && this.y >= that.y); + }, + + /** + * Returns new point which is the result of linear interpolation with this one and another one + * @param {fabric.Point} that + * @param {Number} t , position of interpolation, between 0 and 1 default 0.5 + * @return {fabric.Point} + */ + lerp: function (that, t) { + if (typeof t === 'undefined') { + t = 0.5; + } + t = Math.max(Math.min(1, t), 0); + return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); + }, + + /** + * Returns distance from this point and another one + * @param {fabric.Point} that + * @return {Number} + */ + distanceFrom: function (that) { + var dx = this.x - that.x, + dy = this.y - that.y; + return Math.sqrt(dx * dx + dy * dy); + }, + + /** + * Returns the point between this point and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + midPointFrom: function (that) { + return this.lerp(that); + }, + + /** + * Returns a new point which is the min of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + min: function (that) { + return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); + }, + + /** + * Returns a new point which is the max of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + max: function (that) { + return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); + }, + + /** + * Returns string representation of this point + * @return {String} + */ + toString: function () { + return this.x + ',' + this.y; + }, + + /** + * Sets x/y of this point + * @param {Number} x + * @param {Number} y + * @chainable + */ + setXY: function (x, y) { + this.x = x; + this.y = y; + return this; + }, + + /** + * Sets x of this point + * @param {Number} x + * @chainable + */ + setX: function (x) { + this.x = x; + return this; + }, + + /** + * Sets y of this point + * @param {Number} y + * @chainable + */ + setY: function (y) { + this.y = y; + return this; + }, + + /** + * Sets x/y of this point from another point + * @param {fabric.Point} that + * @chainable + */ + setFromPoint: function (that) { + this.x = that.x; + this.y = that.y; + return this; + }, + + /** + * Swaps x/y of this point and another point + * @param {fabric.Point} that + */ + swap: function (that) { + var x = this.x, + y = this.y; + this.x = that.x; + this.y = that.y; + that.x = x; + that.y = y; + }, + + /** + * return a cloned instance of the point + * @return {fabric.Point} + */ + clone: function () { + return new Point(this.x, this.y); + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + constructor: Intersection, + + /** + * Appends a point to intersection + * @param {fabric.Point} point + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoint: function (point) { + this.points.push(point); + return this; + }, + + /** + * Appends points to intersection + * @param {Array} points + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + return this; + } + }; + + /** + * Checks if one line intersects another + * TODO: rename in intersectSegmentSegment + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (uB !== 0) { + var ua = uaT / uB, + ub = ubT / uB; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection('Intersection'); + result.appendPoint(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (uaT === 0 || ubT === 0) { + result = new Intersection('Coincident'); + } + else { + result = new Intersection('Parallel'); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * TODO: rename in intersectSegmentPolygon + * fix detection of coincident + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1, a2, points) { + var result = new Intersection(), + length = points.length, + b1, b2, inter, i; + + for (i = 0; i < length; i++) { + b1 = points[i]; + b2 = points[(i + 1) % length]; + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length, i; + + for (i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i + 1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {fabric.Point} r1 + * @param {fabric.Point} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * Color class + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class fabric.Color + * @param {String} color optional in hex or rgb(a) or hsl format or from known color list + * @return {fabric.Color} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} + */ + function Color(color) { + if (!color) { + this.setSource([0, 0, 0, 1]); + } + else { + this._tryParsingColor(color); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @lends fabric.Color.prototype */ { + + /** + * @private + * @param {String|Array} color Color value to parse + */ + _tryParsingColor: function(color) { + var source; + + if (color in Color.colorNameMap) { + color = Color.colorNameMap[color]; + } + + if (color === 'transparent') { + source = [255, 255, 255, 0]; + } + + if (!source) { + source = Color.sourceFromHex(color); + } + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (!source) { + //if color is not recognize let's make black as canvas does + source = [0, 0, 0, 1]; + } + if (source) { + this.setSource(source); + } + }, + + /** + * Adapted from https://github.com/mjijackson + * @private + * @param {Number} r Red color value + * @param {Number} g Green color value + * @param {Number} b Blue color value + * @return {Array} Hsl color + */ + _rgbToHsl: function(r, g, b) { + r /= 255; g /= 255; b /= 255; + + var h, s, l, + max = fabric.util.array.max([r, g, b]), + min = fabric.util.array.min([r, g, b]); + + l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [ + Math.round(h * 360), + Math.round(s * 100), + Math.round(l * 100) + ]; + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color representation in RGB format + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color representation in RGBA format + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color representation in HSL format + * @return {String} ex: hsl(0-360,0%-100%,0%-100%) + */ + toHsl: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; + }, + + /** + * Returns color representation in HSLA format + * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) + */ + toHsla: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; + }, + + /** + * Returns color representation in HEX format + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(), r, g, b; + + r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Returns color representation in HEXA format + * @return {String} ex: FF5555CC + */ + toHexa: function() { + var source = this.getSource(), a; + + a = Math.round(source[3] * 255); + a = a.toString(16); + a = (a.length === 1) ? ('0' + a) : a; + + return this.toHex() + a.toUpperCase(); + }, + + /** + * Gets value of alpha channel for this color + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @param {Number} alpha Alpha value 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @return {fabric.Color} thisArg + */ + toGrayscale: function() { + var source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Transforms color to its black and white representation + * @param {Number} threshold + * @return {fabric.Color} thisArg + */ + toBlackWhite: function(threshold) { + var source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3]; + + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Overlays color with another color + * @param {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); + } + + var result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(), i; + + for (i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); + } + + result[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) + * @static + * @field + * @memberOf fabric.Color + */ + // eslint-disable-next-line max-len + fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HEX format (ex: #FF5544CC, #FF5555, 010155, aff) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; + + /** + * Map of the 148 color names with HEX code + * @static + * @field + * @memberOf fabric.Color + * @see: https://www.w3.org/TR/css3-color/#svg-color + */ + fabric.Color.colorNameMap = { + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aqua: '#00FFFF', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + black: '#000000', + blanchedalmond: '#FFEBCD', + blue: '#0000FF', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + cyan: '#00FFFF', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgrey: '#A9A9A9', + darkgreen: '#006400', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + fuchsia: '#FF00FF', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + gray: '#808080', + grey: '#808080', + green: '#008000', + greenyellow: '#ADFF2F', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgray: '#D3D3D3', + lightgrey: '#D3D3D3', + lightgreen: '#90EE90', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + lime: '#00FF00', + limegreen: '#32CD32', + linen: '#FAF0E6', + magenta: '#FF00FF', + maroon: '#800000', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + navy: '#000080', + oldlace: '#FDF5E6', + olive: '#808000', + olivedrab: '#6B8E23', + orange: '#FFA500', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + purple: '#800080', + rebeccapurple: '#663399', + red: '#FF0000', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + silver: '#C0C0C0', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + teal: '#008080', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + white: '#FFFFFF', + whitesmoke: '#F5F5F5', + yellow: '#FFFF00', + yellowgreen: '#9ACD32' + }; + + /** + * @private + * @param {Number} p + * @param {Number} q + * @param {Number} t + * @return {Number} + */ + function hue2rgb(p, q, t) { + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } + return p; + } + + /** + * Returns new color object, when given a color in RGB format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) + * @return {Array} source + */ + fabric.Color.sourceFromRgb = function(color) { + var match = color.match(Color.reRGBa); + if (match) { + var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), + g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), + b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); + + return [ + parseInt(r, 10), + parseInt(g, 10), + parseInt(b, 10), + match[4] ? parseFloat(match[4]) : 1 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HSL format + * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) + * @memberOf fabric.Color + * @return {fabric.Color} + */ + fabric.Color.fromHsl = function(color) { + return Color.fromSource(Color.sourceFromHsl(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. + * Adapted from https://github.com/mjijackson + * @memberOf fabric.Color + * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) + * @return {Array} source + * @see http://http://www.w3.org/TR/css3-color/#hsl-color + */ + fabric.Color.sourceFromHsl = function(color) { + var match = color.match(Color.reHSLa); + if (!match) { + return; + } + + var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, + s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), + l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), + r, g, b; + + if (s === 0) { + r = g = b = l; + } + else { + var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, + p = l * 2 - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255), + match[4] ? parseFloat(match[4]) : 1 + ]; + }; + + /** + * Returns new color object, when given a color in HSLA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromHsla = Color.fromHsl; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color Color value ex: FF5555 + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color ex: FF5555 or FF5544CC (RGBa) + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3 || value.length === 4), + isRGBa = (value.length === 8 || value.length === 4), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), + a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + parseFloat((parseInt(a, 16) / 255).toFixed(2)) + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @memberOf fabric.Color + * @param {Array} source + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + scaleMap = ['e', 'se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'], + skewMap = ['ns', 'nesw', 'ew', 'nwse'], + controls = {}, + LEFT = 'left', TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', CENTER = 'center', + opposite = { + top: BOTTOM, + bottom: TOP, + left: RIGHT, + right: LEFT, + center: CENTER, + }, radiansToDegrees = fabric.util.radiansToDegrees, + sign = (Math.sign || function(x) { return ((x > 0) - (x < 0)) || +x; }); + + /** + * Combine control position and object angle to find the control direction compared + * to the object center. + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + * @param {fabric.Control} control the control class + * @return {Number} 0 - 7 a quadrant number + */ + function findCornerQuadrant(fabricObject, control) { + var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + return Math.round((cornerAngle % 360) / 45); + } + + function fireEvent(eventName, options) { + var target = options.transform.target, + canvas = target.canvas, + canvasOptions = fabric.util.object.clone(options); + canvasOptions.target = target; + canvas && canvas.fire('object:' + eventName, canvasOptions); + target.fire(eventName, options); + } + + /** + * Inspect event and fabricObject properties to understand if the scaling action + * @param {Event} eventData from the user action + * @param {fabric.Object} fabricObject the fabric object about to scale + * @return {Boolean} true if scale is proportional + */ + function scaleIsProportional(eventData, fabricObject) { + var canvas = fabricObject.canvas, uniScaleKey = canvas.uniScaleKey, + uniformIsToggled = eventData[uniScaleKey]; + return (canvas.uniformScaling && !uniformIsToggled) || + (!canvas.uniformScaling && uniformIsToggled); + } + + /** + * Checks if transform is centered + * @param {Object} transform transform data + * @return {Boolean} true if transform is centered + */ + function isTransformCentered(transform) { + return transform.originX === CENTER && transform.originY === CENTER; + } + + /** + * Inspect fabricObject to understand if the current scaling action is allowed + * @param {fabric.Object} fabricObject the fabric object about to scale + * @param {String} by 'x' or 'y' or '' + * @param {Boolean} scaleProportionally true if we are trying to scale proportionally + * @return {Boolean} true if scaling is not allowed at current conditions + */ + function scalingIsForbidden(fabricObject, by, scaleProportionally) { + var lockX = fabricObject.lockScalingX, lockY = fabricObject.lockScalingY; + if (lockX && lockY) { + return true; + } + if (!by && (lockX || lockY) && scaleProportionally) { + return true; + } + if (lockX && by === 'x') { + return true; + } + if (lockY && by === 'y') { + return true; + } + return false; + } + + /** + * return the correct cursor style for the scale action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed', + scaleProportionally = scaleIsProportional(eventData, fabricObject), + by = ''; + if (control.x !== 0 && control.y === 0) { + by = 'x'; + } + else if (control.x === 0 && control.y !== 0) { + by = 'y'; + } + if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control); + return scaleMap[n] + '-resize'; + } + + /** + * return the correct cursor style for the skew action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function skewCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed'; + if (control.x !== 0 && fabricObject.lockSkewingY) { + return notAllowed; + } + if (control.y !== 0 && fabricObject.lockSkewingX) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control) % 4; + return skewMap[n] + '-resize'; + } + + /** + * Combine skew and scale style handlers to cover fabric standard use case + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleSkewCursorStyleHandler(eventData, control, fabricObject) { + if (eventData[fabricObject.canvas.altActionKey]) { + return controls.skewCursorStyleHandler(eventData, control, fabricObject); + } + return controls.scaleCursorStyleHandler(eventData, control, fabricObject); + } + + /** + * Inspect event, control and fabricObject to return the correct action name + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} an action name + */ + function scaleOrSkewActionName(eventData, control, fabricObject) { + var isAlternative = eventData[fabricObject.canvas.altActionKey]; + if (control.x === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewX' : 'scaleY'; + } + if (control.y === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewY' : 'scaleX'; + } + } + + /** + * Find the correct style for the control that is used for rotation. + * this function is very simple and it just take care of not-allowed or standard cursor + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function rotationStyleHandler(eventData, control, fabricObject) { + if (fabricObject.lockRotation) { + return 'not-allowed'; + } + return control.cursorStyle; + } + + function commonEventInfo(eventData, transform, x, y) { + return { + e: eventData, + transform: transform, + pointer: { + x: x, + y: y, + } + }; + } + + /** + * Wrap an action handler with saving/restoring object position on the transform. + * this is the code that permits to objects to keep their position while transforming. + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFixedAnchor(actionHandler) { + return function(eventData, transform, x, y) { + var target = transform.target, centerPoint = target.getCenterPoint(), + constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), + actionPerformed = actionHandler(eventData, transform, x, y); + target.setPositionByOrigin(constraint, transform.originX, transform.originY); + return actionPerformed; + }; + } + + /** + * Wrap an action handler with firing an event if the action is performed + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFireEvent(eventName, actionHandler) { + return function(eventData, transform, x, y) { + var actionPerformed = actionHandler(eventData, transform, x, y); + if (actionPerformed) { + fireEvent(eventName, commonEventInfo(eventData, transform, x, y)); + } + return actionPerformed; + }; + } + + /** + * Transforms a point described by x and y in a distance from the top left corner of the object + * bounding box. + * @param {Object} transform + * @param {String} originX + * @param {String} originY + * @param {number} x + * @param {number} y + * @return {Fabric.Point} the normalized point + */ + function getLocalPoint(transform, originX, originY, x, y) { + var target = transform.target, + control = target.controls[transform.corner], + zoom = target.canvas.getZoom(), + padding = target.padding / zoom, + localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); + if (localPoint.x >= padding) { + localPoint.x -= padding; + } + if (localPoint.x <= -padding) { + localPoint.x += padding; + } + if (localPoint.y >= padding) { + localPoint.y -= padding; + } + if (localPoint.y <= padding) { + localPoint.y += padding; + } + localPoint.x -= control.offsetX; + localPoint.y -= control.offsetY; + return localPoint; + } + + /** + * Detect if the fabric object is flipped on one side. + * @param {fabric.Object} target + * @return {Boolean} true if one flip, but not two. + */ + function targetHasOneFlip(target) { + return target.flipX !== target.flipY; + } + + /** + * Utility function to compensate the scale factor when skew is applied on both axes + * @private + */ + function compensateScaleForSkew(target, oppositeSkew, scaleToCompensate, axis, reference) { + if (target[oppositeSkew] !== 0) { + var newDim = target._getTransformedDimensions()[axis]; + var newValue = reference / newDim * target[scaleToCompensate]; + target.set(scaleToCompensate, newValue); + } + } + + /** + * Action handler for skewing on the X axis + * @private + */ + function skewObjectX(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(0, target.skewY), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.x * 2) - dimNoSkew.x, + currentSkew = target.skewX, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleX), (dimNoSkew.y / target.scaleY)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().y; + target.set('skewX', newSkew); + compensateScaleForSkew(target, 'skewY', 'scaleY', 'y', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Action handler for skewing on the Y axis + * @private + */ + function skewObjectY(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(target.skewX, 0), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.y * 2) - dimNoSkew.y, + currentSkew = target.skewY, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleY), (dimNoSkew.x / target.scaleX)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().x; + target.set('skewY', newSkew); + compensateScaleForSkew(target, 'skewX', 'scaleX', 'x', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerX(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewX > 0 and originY bottom we anchor on right + // if skewX > 0 and originY top we anchor on left + // if skewX < 0 and originY bottom we anchor on left + // if skewX < 0 and originY top we anchor on right + // if skewX is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewX, originX, originY = transform.originY; + if (target.lockSkewingX) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.x > 0) { + // we are pulling right, anchor left; + originX = LEFT; + } + else { + // we are pulling right, anchor right + originX = RIGHT; + } + } + else { + if (currentSkew > 0) { + originX = originY === TOP ? LEFT : RIGHT; + } + if (currentSkew < 0) { + originX = originY === TOP ? RIGHT : LEFT; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originX = originX === LEFT ? RIGHT : LEFT; + } + } + + // once we have the origin, we find the anchor point + transform.originX = originX; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectX)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerY(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewY > 0 and originX left we anchor on top + // if skewY > 0 and originX right we anchor on bottom + // if skewY < 0 and originX left we anchor on bottom + // if skewY < 0 and originX right we anchor on top + // if skewY is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewY, originY, originX = transform.originX; + if (target.lockSkewingY) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.y > 0) { + // we are pulling down, anchor up; + originY = TOP; + } + else { + // we are pulling up, anchor down + originY = BOTTOM; + } + } + else { + if (currentSkew > 0) { + originY = originX === LEFT ? TOP : BOTTOM; + } + if (currentSkew < 0) { + originY = originX === LEFT ? BOTTOM : TOP; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originY = originY === TOP ? BOTTOM : TOP; + } + } + + // once we have the origin, we find the anchor point + transform.originY = originY; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectY)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Action handler for rotation and snapping, without anchor point. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + * @private + */ + function rotationWithSnapping(eventData, transform, x, y) { + var t = transform, + target = t.target, + pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + + if (target.lockRotation) { + return false; + } + + var lastAngle = Math.atan2(t.ey - pivotPoint.y, t.ex - pivotPoint.x), + curAngle = Math.atan2(y - pivotPoint.y, x - pivotPoint.x), + angle = radiansToDegrees(curAngle - lastAngle + t.theta), + hasRotated = true; + + if (target.snapAngle > 0) { + var snapAngle = target.snapAngle, + snapThreshold = target.snapThreshold || snapAngle, + rightAngleLocked = Math.ceil(angle / snapAngle) * snapAngle, + leftAngleLocked = Math.floor(angle / snapAngle) * snapAngle; + + if (Math.abs(angle - leftAngleLocked) < snapThreshold) { + angle = leftAngleLocked; + } + else if (Math.abs(angle - rightAngleLocked) < snapThreshold) { + angle = rightAngleLocked; + } + } + + // normalize angle to positive value + if (angle < 0) { + angle = 360 + angle; + } + angle %= 360; + + hasRotated = target.angle !== angle; + target.angle = angle; + return hasRotated; + } + + /** + * Basic scaling logic, reused with different constrain for scaling X,Y, freely or equally. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @param {Object} options additional information for scaling + * @param {String} options.by 'x', 'y', 'equally' or '' to indicate type of scaling + * @return {Boolean} true if some change happened + * @private + */ + function scaleObject(eventData, transform, x, y, options) { + options = options || {}; + var target = transform.target, + lockScalingX = target.lockScalingX, lockScalingY = target.lockScalingY, + by = options.by, newPoint, scaleX, scaleY, dim, + scaleProportionally = scaleIsProportional(eventData, target), + forbidScaling = scalingIsForbidden(target, by, scaleProportionally), + signX, signY, gestureScale = transform.gestureScale; + + if (forbidScaling) { + return false; + } + if (gestureScale) { + scaleX = transform.scaleX * gestureScale; + scaleY = transform.scaleY * gestureScale; + } + else { + newPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y); + // use of sign: We use sign to detect change of direction of an action. sign usually change when + // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling + // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily + // cross many time the origin point and flip the object. so we need a way to filter out the noise. + // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. + signX = by !== 'y' ? sign(newPoint.x) : 1; + signY = by !== 'x' ? sign(newPoint.y) : 1; + if (!transform.signX) { + transform.signX = signX; + } + if (!transform.signY) { + transform.signY = signY; + } + + if (target.lockScalingFlip && + (transform.signX !== signX || transform.signY !== signY) + ) { + return false; + } + + dim = target._getTransformedDimensions(); + // missing detection of flip and logic to switch the origin + if (scaleProportionally && !by) { + // uniform scaling + var distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), + original = transform.original, + originalDistance = Math.abs(dim.x * original.scaleX / target.scaleX) + + Math.abs(dim.y * original.scaleY / target.scaleY), + scale = distance / originalDistance; + scaleX = original.scaleX * scale; + scaleY = original.scaleY * scale; + } + else { + scaleX = Math.abs(newPoint.x * target.scaleX / dim.x); + scaleY = Math.abs(newPoint.y * target.scaleY / dim.y); + } + // if we are scaling by center, we need to double the scale + if (isTransformCentered(transform)) { + scaleX *= 2; + scaleY *= 2; + } + if (transform.signX !== signX && by !== 'y') { + transform.originX = opposite[transform.originX]; + scaleX *= -1; + transform.signX = signX; + } + if (transform.signY !== signY && by !== 'x') { + transform.originY = opposite[transform.originY]; + scaleY *= -1; + transform.signY = signY; + } + } + // minScale is taken are in the setter. + var oldScaleX = target.scaleX, oldScaleY = target.scaleY; + if (!by) { + !lockScalingX && target.set('scaleX', scaleX); + !lockScalingY && target.set('scaleY', scaleY); + } + else { + // forbidden cases already handled on top here. + by === 'x' && target.set('scaleX', scaleX); + by === 'y' && target.set('scaleY', scaleY); + } + return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; + } + + /** + * Generic scaling logic, to scale from corners either equally or freely. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectFromCorner(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y); + } + + /** + * Scaling logic for the X axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectX(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'x' }); + } + + /** + * Scaling logic for the Y axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectY(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'y' }); + } + + /** + * Composed action handler to either scale Y or skew X + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingYOrSkewingX(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerX(eventData, transform, x, y); + } + return controls.scalingY(eventData, transform, x, y); + } + + /** + * Composed action handler to either scale X or skew Y + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingXOrSkewingY(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerY(eventData, transform, x, y); + } + return controls.scalingX(eventData, transform, x, y); + } + + /** + * Action handler to change textbox width + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function changeWidth(eventData, transform, x, y) { + var target = transform.target, localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), + multiplier = isTransformCentered(transform) ? 2 : 1, + oldWidth = target.width, + newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; + target.set('width', Math.max(newWidth, 0)); + return oldWidth !== newWidth; + } + + /** + * Action handler + * @private + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if the translation occurred + */ + function dragHandler(eventData, transform, x, y) { + var target = transform.target, + newLeft = x - transform.offsetX, + newTop = y - transform.offsetY, + moveX = !target.get('lockMovementX') && target.left !== newLeft, + moveY = !target.get('lockMovementY') && target.top !== newTop; + moveX && target.set('left', newLeft); + moveY && target.set('top', newTop); + if (moveX || moveY) { + fireEvent('moving', commonEventInfo(eventData, transform, x, y)); + } + return moveX || moveY; + } + + controls.scaleCursorStyleHandler = scaleCursorStyleHandler; + controls.skewCursorStyleHandler = skewCursorStyleHandler; + controls.scaleSkewCursorStyleHandler = scaleSkewCursorStyleHandler; + controls.rotationWithSnapping = wrapWithFireEvent('rotating', wrapWithFixedAnchor(rotationWithSnapping)); + controls.scalingEqually = wrapWithFireEvent('scaling', wrapWithFixedAnchor( scaleObjectFromCorner)); + controls.scalingX = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectX)); + controls.scalingY = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectY)); + controls.scalingYOrSkewingX = scalingYOrSkewingX; + controls.scalingXOrSkewingY = scalingXOrSkewingY; + controls.changeWidth = wrapWithFireEvent('resizing', wrapWithFixedAnchor(changeWidth)); + controls.skewHandlerX = skewHandlerX; + controls.skewHandlerY = skewHandlerY; + controls.dragHandler = dragHandler; + controls.scaleOrSkewActionName = scaleOrSkewActionName; + controls.rotationStyleHandler = rotationStyleHandler; + controls.fireEvent = fireEvent; + controls.wrapWithFixedAnchor = wrapWithFixedAnchor; + controls.wrapWithFireEvent = wrapWithFireEvent; + controls.getLocalPoint = getLocalPoint; + fabric.controlsUtils = controls; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + degreesToRadians = fabric.util.degreesToRadians, + controls = fabric.controlsUtils; + + /** + * Render a round control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderCircleControl (ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor), + myLeft = left, + myTop = top, size; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // as soon as fabric react v5, remove ie11, use proper ellipse code. + if (xSize > ySize) { + size = xSize; + ctx.scale(1.0, ySize / xSize); + myTop = top * xSize / ySize; + } + else if (ySize > xSize) { + size = ySize; + ctx.scale(xSize / ySize, 1.0); + myLeft = left * ySize / xSize; + } + else { + size = xSize; + } + // this is still wrong + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false); + ctx[methodName](); + if (stroke) { + ctx.stroke(); + } + ctx.restore(); + } + + /** + * Render a square control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderSquareControl(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && ( + styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor + ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // this is still wrong + ctx.lineWidth = 1; + ctx.translate(left, top); + ctx.rotate(degreesToRadians(fabricObject.angle)); + // this does not work, and fixed with ( && ) does not make sense. + // to have real transparent corners we need the controls on upperCanvas + // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize); + if (stroke) { + ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + } + ctx.restore(); + } + + controls.renderCircleControl = renderCircleControl; + controls.renderSquareControl = renderSquareControl; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + function Control(options) { + for (var i in options) { + this[i] = options[i]; + } + } + + fabric.Control = Control; + + fabric.Control.prototype = /** @lends fabric.Control.prototype */ { + + /** + * keep track of control visibility. + * mainly for backward compatibility. + * if you do not want to see a control, you can remove it + * from the controlset. + * @type {Boolean} + * @default true + */ + visible: true, + + /** + * Name of the action that the control will likely execute. + * This is optional. FabricJS uses to identify what the user is doing for some + * extra optimizations. If you are writing a custom control and you want to know + * somewhere else in the code what is going on, you can use this string here. + * you can also provide a custom getActionName if your control run multiple actions + * depending on some external state. + * default to scale since is the most common, used on 4 corners by default + * @type {String} + * @default 'scale' + */ + actionName: 'scale', + + /** + * Drawing angle of the control. + * NOT used for now, but name marked as needed for internal logic + * example: to reuse the same drawing function for different rotated controls + * @type {Number} + * @default 0 + */ + angle: 0, + + /** + * Relative position of the control. X + * 0,0 is the center of the Object, while -0.5 (left) or 0.5 (right) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + x: 0, + + /** + * Relative position of the control. Y + * 0,0 is the center of the Object, while -0.5 (top) or 0.5 (bottom) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + y: 0, + + /** + * Horizontal offset of the control from the defined position. In pixels + * Positive offset moves the control to the right, negative to the left. + * It used when you want to have position of control that does not scale with + * the bounding box. Example: rotation control is placed at x:0, y: 0.5 on + * the boundindbox, with an offset of 30 pixels vertically. Those 30 pixels will + * stay 30 pixels no matter how the object is big. Another example is having 2 + * controls in the corner, that stay in the same position when the object scale. + * of the bounding box. + * @type {Number} + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset of the control from the defined position. In pixels + * Positive offset moves the control to the bottom, negative to the top. + * @type {Number} + * @default 0 + */ + offsetY: 0, + + /** + * Sets the length of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeX: null, + + /** + * Sets the height of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeY: null, + + /** + * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeX: null, + + /** + * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeY: null, + + /** + * Css cursor style to display when the control is hovered. + * if the method `cursorStyleHandler` is provided, this property is ignored. + * @type {String} + * @default 'crosshair' + */ + cursorStyle: 'crosshair', + + /** + * If controls has an offsetY or offsetX, draw a line that connects + * the control to the bounding box + * @type {Boolean} + * @default false + */ + withConnection: false, + + /** + * The control actionHandler, provide one to handle action ( control being moved ) + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + actionHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control handler for mouse down, provide one to handle mouse down on control + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseDownHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control mouseUpHandler, provide one to handle an effect on mouse up. + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseUpHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * Returns control actionHandler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getActionHandler: function(/* eventData, fabricObject, control */) { + return this.actionHandler; + }, + + /** + * Returns control mouseDown handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseDownHandler: function(/* eventData, fabricObject, control */) { + return this.mouseDownHandler; + }, + + /** + * Returns control mouseUp handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseUpHandler: function(/* eventData, fabricObject, control */) { + return this.mouseUpHandler; + }, + + /** + * Returns control cursorStyle for css using cursorStyle. If you need a more elaborate + * function you can pass one in the constructor + * the cursorStyle property + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + cursorStyleHandler: function(eventData, control /* fabricObject */) { + return control.cursorStyle; + }, + + /** + * Returns the action name. The basic implementation just return the actionName property. + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + getActionName: function(eventData, control /* fabricObject */) { + return control.actionName; + }, + + /** + * Returns controls visibility + * @param {fabric.Object} object on which the control is displayed + * @param {String} controlKey key where the control is memorized on the + * @return {Boolean} + */ + getVisibility: function(fabricObject, controlKey) { + var objectVisibility = fabricObject._controlsVisibility; + if (objectVisibility && typeof objectVisibility[controlKey] !== 'undefined') { + return objectVisibility[controlKey]; + } + return this.visible; + }, + + /** + * Sets controls visibility + * @param {Boolean} visibility for the object + * @return {Void} + */ + setVisibility: function(visibility /* name, fabricObject */) { + this.visible = visibility; + }, + + + positionHandler: function(dim, finalMatrix /*, fabricObject, currentControl */) { + var point = fabric.util.transformPoint({ + x: this.x * dim.x + this.offsetX, + y: this.y * dim.y + this.offsetY }, finalMatrix); + return point; + }, + + /** + * Returns the coords for this control based on object values. + * @param {Number} objectAngle angle from the fabric object holding the control + * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if + * isTouch is true) + * @param {Number} centerX x coordinate where the control center should be + * @param {Number} centerY y coordinate where the control center should be + * @param {boolean} isTouch true if touch corner, false if normal corner + */ + calcCornerCoords: function(objectAngle, objectCornerSize, centerX, centerY, isTouch) { + var cosHalfOffset, + sinHalfOffset, + cosHalfOffsetComp, + sinHalfOffsetComp, + xSize = (isTouch) ? this.touchSizeX : this.sizeX, + ySize = (isTouch) ? this.touchSizeY : this.sizeY; + if (xSize && ySize && xSize !== ySize) { + // handle rectangular corners + var controlTriangleAngle = Math.atan2(ySize, xSize); + var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; + var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta); + // use complementary angle for two corners + cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp); + sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp); + } + else { + // handle square corners + // use default object corner size unless size is defined + var cornerSize = (xSize && ySize) ? xSize : objectCornerSize; + /* 0.7071067812 stands for sqrt(2)/2 */ + cornerHypotenuse = cornerSize * 0.7071067812; + // complementary angles are equal since they're both 45 degrees + var newTheta = fabric.util.degreesToRadians(45 - objectAngle); + cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta); + } + + return { + tl: { + x: centerX - sinHalfOffsetComp, + y: centerY - cosHalfOffsetComp, + }, + tr: { + x: centerX + cosHalfOffset, + y: centerY - sinHalfOffset, + }, + bl: { + x: centerX - cosHalfOffset, + y: centerY + sinHalfOffset, + }, + br: { + x: centerX + sinHalfOffsetComp, + y: centerY + cosHalfOffsetComp, + }, + }; + }, + + /** + * Render function for the control. + * When this function runs the context is unscaled. unrotate. Just retina scaled. + * all the functions will have to translate to the point left,top before starting Drawing + * if they want to draw a control where the position is detected. + * left and top are the result of the positionHandler function + * @param {RenderingContext2D} ctx the context where the control will be drawn + * @param {Number} left position of the canvas where we are about to render the control. + * @param {Number} top position of the canvas where we are about to render the control. + * @param {Object} styleOverride + * @param {fabric.Object} fabricObject the object where the control is about to be rendered + */ + render: function(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + switch (styleOverride.cornerStyle || fabricObject.cornerStyle) { + case 'circle': + fabric.controlsUtils.renderCircleControl.call(this, ctx, left, top, styleOverride, fabricObject); + break; + default: + fabric.controlsUtils.renderSquareControl.call(this, ctx, left, top, styleOverride, fabricObject); + } + }, + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + /* _FROM_SVG_START_ */ + function getColorStop(el, multiplier) { + var style = el.getAttribute('style'), + offset = el.getAttribute('offset') || 0, + color, colorAlpha, opacity, i; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length - 1] === '') { + keyValuePairs.pop(); + } + + for (i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + color = value; + } + else if (key === 'stop-opacity') { + opacity = value; + } + } + } + + if (!color) { + color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; + } + if (!opacity) { + opacity = el.getAttribute('stop-opacity'); + } + + color = new fabric.Color(color); + colorAlpha = color.getAlpha(); + opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); + opacity *= colorAlpha * multiplier; + + return { + offset: offset, + color: color.toRgb(), + opacity: opacity + }; + } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } + /* _FROM_SVG_END_ */ + + var clone = fabric.util.object.clone; + + /** + * Gradient class + * @class fabric.Gradient + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition + */ + fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { + + /** + * Horizontal offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetY: 0, + + /** + * A transform matrix to apply to the gradient before painting. + * Imported from svg gradients, is not applied with the current transform in the center. + * Before this transform is applied, the origin point is at the top left corner of the object + * plus the addition of offsetY and offsetX. + * @type Number[] + * @default null + */ + gradientTransform: null, + + /** + * coordinates units for coords. + * If `pixels`, the number of coords are in the same unit of width / height. + * If set as `percentage` the coords are still a number, but 1 means 100% of width + * for the X and 100% of the height for the y. It can be bigger than 1 and negative. + * allowed values pixels or percentage. + * @type String + * @default 'pixels' + */ + gradientUnits: 'pixels', + + /** + * Gradient type linear or radial + * @type String + * @default 'pixels' + */ + type: 'linear', + + /** + * Constructor + * @param {Object} options Options object with type, coords, gradientUnits and colorStops + * @param {Object} [options.type] gradient type linear or radial + * @param {Object} [options.gradientUnits] gradient units + * @param {Object} [options.offsetX] SVG import compatibility + * @param {Object} [options.offsetY] SVG import compatibility + * @param {Object[]} options.colorStops contains the colorstops. + * @param {Object} options.coords contains the coords of the gradient + * @param {Number} [options.coords.x1] X coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.y1] Y coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.x2] X coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.y2] Y coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.r1] only for radial gradient, radius of the inner circle + * @param {Number} [options.coords.r2] only for radial gradient, radius of the external circle + * @return {fabric.Gradient} thisArg + */ + initialize: function(options) { + options || (options = { }); + options.coords || (options.coords = { }); + + var coords, _this = this; + + // sets everything, then coords and colorstops get sets again + Object.keys(options).forEach(function(option) { + _this[option] = options[option]; + }); + + if (this.id) { + this.id += '_' + fabric.Object.__uid++; + } + else { + this.id = fabric.Object.__uid++; + } + + coords = { + x1: options.coords.x1 || 0, + y1: options.coords.y1 || 0, + x2: options.coords.x2 || 0, + y2: options.coords.y2 || 0 + }; + + if (this.type === 'radial') { + coords.r1 = options.coords.r1 || 0; + coords.r2 = options.coords.r2 || 0; + } + + this.coords = coords; + this.colorStops = options.colorStops.slice(); + }, + + /** + * Adds another colorStop + * @param {Object} colorStop Object with offset and color + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStops) { + for (var position in colorStops) { + var color = new fabric.Color(colorStops[position]); + this.colorStops.push({ + offset: parseFloat(position), + color: color.toRgb(), + opacity: color.getAlpha() + }); + } + return this; + }, + + /** + * Returns object representation of a gradient + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} + */ + toObject: function(propertiesToInclude) { + var object = { + type: this.type, + coords: this.coords, + colorStops: this.colorStops, + offsetX: this.offsetX, + offsetY: this.offsetY, + gradientUnits: this.gradientUnits, + gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an gradient + * @param {Object} object Object to create a gradient for + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object, options) { + var coords = clone(this.coords, true), i, len, options = options || {}, + markup, commonAttributes, colorStops = clone(this.colorStops, true), + needsSwap = coords.r1 > coords.r2, + transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), + offsetX = -this.offsetX, offsetY = -this.offsetY, + withViewport = !!options.additionalTransform, + gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; + // colorStops must be sorted ascending + colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (gradientUnits === 'objectBoundingBox') { + offsetX /= object.width; + offsetY /= object.height; + } + else { + offsetX += object.width / 2; + offsetY += object.height / 2; + } + if (object.type === 'path' && this.gradientUnits !== 'percentage') { + offsetX -= object.pathOffset.x; + offsetY -= object.pathOffset.y; + } + + + transform[4] -= offsetX; + transform[5] -= offsetY; + + commonAttributes = 'id="SVGID_' + this.id + + '" gradientUnits="' + gradientUnits + '"'; + commonAttributes += ' gradientTransform="' + (withViewport ? + options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; + + if (this.type === 'linear') { + markup = [ + '\n' + ]; + } + else if (this.type === 'radial') { + // svg radial gradient has just 1 radius. the biggest. + markup = [ + '\n' + ]; + } + + if (this.type === 'radial') { + if (needsSwap) { + // svg goes from internal to external radius. if radius are inverted, swap color stops. + colorStops = colorStops.concat(); + colorStops.reverse(); + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset = 1 - colorStops[i].offset; + } + } + var minRadius = Math.min(coords.r1, coords.r2); + if (minRadius > 0) { + // i have to shift all colorStops and add new one in 0. + var maxRadius = Math.max(coords.r1, coords.r2), + percentageShift = minRadius / maxRadius; + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); + } + } + } + + for (i = 0, len = colorStops.length; i < len; i++) { + var colorStop = colorStops[i]; + markup.push( + '\n' + ); + } + + markup.push((this.type === 'linear' ? '\n' : '\n')); + + return markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {CanvasGradient} + */ + toLive: function(ctx) { + var gradient, coords = fabric.util.object.clone(this.coords), i, len; + + if (!this.type) { + return; + } + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + coords.x1, coords.y1, coords.x2, coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); + } + + for (i = 0, len = this.colorStops.length; i < len; i++) { + var color = this.colorStops[i].color, + opacity = this.colorStops[i].opacity, + offset = this.colorStops[i].offset; + + if (typeof opacity !== 'undefined') { + color = new fabric.Color(color).setAlpha(opacity).toRgba(); + } + gradient.addColorStop(offset, color); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /* _FROM_SVG_START_ */ + /** + * Returns {@link fabric.Gradient} instance from an SVG element + * @static + * @memberOf fabric.Gradient + * @param {SVGGradientElement} el SVG gradient element + * @param {fabric.Object} instance + * @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. + * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly gradients + * that uses gradientUnits as 'userSpaceOnUse' and percentages. + * @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg + * @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg + * @param {Object.number} width width part of the svg tag if viewBox is not specified + * @param {Object.number} height height part of the svg tag if viewBox is not specified + * @return {fabric.Gradient} Gradient instance + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement + */ + fromElement: function(el, instance, opacityAttr, svgOptions) { + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + */ + + var multiplier = parseFloat(opacityAttr) / (/%$/.test(opacityAttr) ? 100 : 1); + multiplier = multiplier < 0 ? 0 : multiplier > 1 ? 1 : multiplier; + if (isNaN(multiplier)) { + multiplier = 1; + } + + var colorStopEls = el.getElementsByTagName('stop'), + type, + gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : 'percentage', + gradientTransform = el.getAttribute('gradientTransform') || '', + colorStops = [], + coords, i, offsetX = 0, offsetY = 0, + transformMatrix; + if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { + type = 'linear'; + coords = getLinearCoords(el); + } + else { + type = 'radial'; + coords = getRadialCoords(el); + } + + for (i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i], multiplier)); + } + + transformMatrix = fabric.parseTransformAttribute(gradientTransform); + + __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); + + if (gradientUnits === 'pixels') { + offsetX = -instance.left; + offsetY = -instance.top; + } + + var gradient = new fabric.Gradient({ + id: el.getAttribute('id'), + type: type, + coords: coords, + colorStops: colorStops, + gradientUnits: gradientUnits, + gradientTransform: transformMatrix, + offsetX: offsetX, + offsetY: offsetY, + }); + + return gradient; + } + /* _FROM_SVG_END_ */ + }); + + /** + * @private + */ + function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { + var propValue, finalValue; + Object.keys(options).forEach(function(prop) { + propValue = options[prop]; + if (propValue === 'Infinity') { + finalValue = 1; + } + else if (propValue === '-Infinity') { + finalValue = 0; + } + else { + finalValue = parseFloat(options[prop], 10); + if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { + finalValue *= 0.01; + if (gradientUnits === 'pixels') { + // then we need to fix those percentages here in svg parsing + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + finalValue *= svgOptions.viewBoxWidth || svgOptions.width; + } + if (prop === 'y1' || prop === 'y2') { + finalValue *= svgOptions.viewBoxHeight || svgOptions.height; + } + } + } + } + options[prop] = finalValue; + }); + } +})(); + + +(function() { + + 'use strict'; + + var toFixed = fabric.util.toFixed; + + /** + * Pattern class + * @class fabric.Pattern + * @see {@link http://fabricjs.com/patterns|Pattern demo} + * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition + */ + + + fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { + + /** + * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @type String + * @default + */ + repeat: 'repeat', + + /** + * Pattern horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX: 0, + + /** + * Pattern vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY: 0, + + /** + * crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * transform matrix to change the pattern, imported from svgs. + * @type Array + * @default + */ + patternTransform: null, + + /** + * Constructor + * @param {Object} [options] Options object + * @param {Function} [callback] function to invoke after callback init. + * @return {fabric.Pattern} thisArg + */ + initialize: function(options, callback) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img, isError) { + _this.source = img; + callback && callback(_this, isError); + }, null, this.crossOrigin); + } + }, + + /** + * Returns object representation of a pattern + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of a pattern instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + source, object; + + // element + if (typeof this.source.src === 'string') { + source = this.source.src; + } + // element + else if (typeof this.source === 'object' && this.source.toDataURL) { + source = this.source.toDataURL(); + } + + object = { + type: 'pattern', + source: source, + repeat: this.repeat, + crossOrigin: this.crossOrigin, + offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), + patternTransform: this.patternTransform ? this.patternTransform.concat() : null + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a pattern + * @param {fabric.Object} object + * @return {String} SVG representation of a pattern + */ + toSVG: function(object) { + var patternSource = typeof this.source === 'function' ? this.source() : this.source, + patternWidth = patternSource.width / object.width, + patternHeight = patternSource.height / object.height, + patternOffsetX = this.offsetX / object.width, + patternOffsetY = this.offsetY / object.height, + patternImgSrc = ''; + if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { + patternHeight = 1; + if (patternOffsetY) { + patternHeight += Math.abs(patternOffsetY); + } + } + if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { + patternWidth = 1; + if (patternOffsetX) { + patternWidth += Math.abs(patternOffsetX); + } + + } + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } + + return '\n' + + '\n' + + '\n'; + }, + /* _TO_SVG_END_ */ + + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Returns an instance of CanvasPattern + * @param {CanvasRenderingContext2D} ctx Context to create pattern + * @return {CanvasPattern} + */ + toLive: function(ctx) { + var source = this.source; + // if the image failed to load, return, and allow rest to continue loading + if (!source) { + return ''; + } + + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) { + return ''; + } + if (source.naturalWidth === 0 || source.naturalHeight === 0) { + return ''; + } + } + return ctx.createPattern(source, this.repeat); + } + }); +})(); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Shadow) { + fabric.warn('fabric.Shadow is already defined.'); + return; + } + + /** + * Shadow class + * @class fabric.Shadow + * @see {@link http://fabricjs.com/shadows|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition + */ + fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { + + /** + * Shadow color + * @type String + * @default + */ + color: 'rgb(0,0,0)', + + /** + * Shadow blur + * @type Number + */ + blur: 0, + + /** + * Shadow horizontal offset + * @type Number + * @default + */ + offsetX: 0, + + /** + * Shadow vertical offset + * @type Number + * @default + */ + offsetY: 0, + + /** + * Whether the shadow should affect stroke operations + * @type Boolean + * @default + */ + affectStroke: false, + + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `false`, the shadow will scale with the object. + * When `true`, the shadow's offsetX, offsetY, and blur will not be affected by the object's scale. + * default to false + * @type Boolean + * @default + */ + nonScaling: false, + + /** + * Constructor + * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetY properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px") + * @return {fabric.Shadow} thisArg + */ + initialize: function(options) { + + if (typeof options === 'string') { + options = this._parseShadow(options); + } + + for (var prop in options) { + this[prop] = options[prop]; + } + + this.id = fabric.Object.__uid++; + }, + + /** + * @private + * @param {String} shadow Shadow value to parse + * @return {Object} Shadow object with color, offsetX, offsetY and blur + */ + _parseShadow: function(shadow) { + var shadowStr = shadow.trim(), + offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [], + color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; + + return { + color: color.trim(), + offsetX: parseFloat(offsetsAndBlur[1], 10) || 0, + offsetY: parseFloat(offsetsAndBlur[2], 10) || 0, + blur: parseFloat(offsetsAndBlur[3], 10) || 0 + }; + }, + + /** + * Returns a string representation of an instance + * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow + * @return {String} Returns CSS3 text-shadow declaration + */ + toString: function() { + return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a shadow + * @param {fabric.Object} object + * @return {String} SVG representation of a shadow + */ + toSVG: function(object) { + var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + offset = fabric.util.rotateVector( + { x: this.offsetX, y: this.offsetY }, + fabric.util.degreesToRadians(-object.angle)), + BLUR_BOX = 20, color = new fabric.Color(this.color); + + if (object.width && object.height) { + //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion + // we add some extra space to filter box to contain the blur ( 20 ) + fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + } + if (object.flipX) { + offset.x *= -1; + } + if (object.flipY) { + offset.y *= -1; + } + + return ( + '\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\n' + + '\n'); + }, + /* _TO_SVG_END_ */ + + /** + * Returns object representation of a shadow + * @return {Object} Object representation of a shadow instance + */ + toObject: function() { + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY, + affectStroke: this.affectStroke, + nonScaling: this.nonScaling + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + + ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke', 'nonScaling'].forEach(function(prop) { + if (this[prop] !== proto[prop]) { + obj[prop] = this[prop]; + } + }, this); + + return obj; + } + }); + + /** + * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") + * @static + * @field + * @memberOf fabric.Shadow + */ + // eslint-disable-next-line max-len + fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function () { + + 'use strict'; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + toFixed = fabric.util.toFixed, + transformPoint = fabric.util.transformPoint, + invertTransform = fabric.util.invertTransform, + getNodeCanvas = fabric.util.getNodeCanvas, + createCanvasElement = fabric.util.createCanvasElement, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * Static canvas class + * @class fabric.StaticCanvas + * @mixes fabric.Collection + * @mixes fabric.Observable + * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition + * @fires before:render + * @fires after:render + * @fires canvas:cleared + * @fires object:added + * @fires object:removed + */ + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + }, + + /** + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} + * @default + */ + backgroundColor: '', + + /** + * Background image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as background, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + backgroundImage: null, + + /** + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} + * @default + */ + overlayColor: '', + + /** + * Overlay image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as overlay, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + overlayImage: null, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * if set to false, takes precedence over the object value. + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @type Boolean + * @default + */ + stateful: false, + + /** + * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove}, + * {@link fabric.StaticCanvas.moveTo}, {@link fabric.StaticCanvas.clear} and many more, should also re-render canvas. + * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once + * since the renders are quequed and executed one per frame. + * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) + * Left default to true to do not break documentation and old app, fiddles. + * @type Boolean + * @default + */ + renderOnAddRemove: true, + + /** + * Indicates whether object controls (borders/controls) are rendered above overlay image + * @type Boolean + * @default + */ + controlsAboveOverlay: false, + + /** + * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas + * @type Boolean + * @default + */ + allowTouchScrolling: false, + + /** + * Indicates whether this canvas will use image smoothing, this is on by default in browsers + * @type Boolean + * @default + */ + imageSmoothingEnabled: true, + + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: fabric.iMatrix.concat(), + + /** + * if set to false background image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + backgroundVpt: true, + + /** + * if set to false overlya image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + overlayVpt: true, + + /** + * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + * @type Boolean + * @default + */ + enableRetinaScaling: true, + + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + * @memberOf fabric.StaticCanvas.prototype + */ + vptCoords: { }, + + /** + * Based on vptCoords and object.aCoords, skip rendering of objects that + * are not included in current viewport. + * May greatly help in applications with crowded canvas and use of zoom/pan + * If One of the corner of the bounding box of the object is on the canvas + * the objects get rendered. + * @memberOf fabric.StaticCanvas.prototype + * @type Boolean + * @default + */ + skipOffscreen: true, + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the canvas has rendered, and the context is placed in the + * top left corner of the canvas. + * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true + * @type fabric.Object + */ + clipPath: undefined, + + /** + * @private + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic: function(el, options) { + var cb = this.requestRenderAllBound; + this._objects = []; + this._createLowerCanvas(el); + this._initOptions(options); + // only initialize retina scaling once + if (!this.interactive) { + this._initRetinaScaling(); + } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } + this.calcOffset(); + }, + + /** + * @private + */ + _isRetinaScaling: function() { + return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling); + }, + + /** + * @private + * @return {Number} retinaScaling if applied, otherwise 1; + */ + getRetinaScaling: function() { + return this._isRetinaScaling() ? fabric.devicePixelRatio : 1; + }, + + /** + * @private + */ + _initRetinaScaling: function() { + if (!this._isRetinaScaling()) { + return; + } + var scaleRatio = fabric.devicePixelRatio; + this.__initRetinaScaling(scaleRatio, this.lowerCanvasEl, this.contextContainer); + if (this.upperCanvasEl) { + this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop); + } + }, + + __initRetinaScaling: function(scaleRatio, canvas, context) { + canvas.setAttribute('width', this.width * scaleRatio); + canvas.setAttribute('height', this.height * scaleRatio); + context.scale(scaleRatio, scaleRatio); + }, + + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + // TODO: fix stretched examples + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to + * @param {Function} callback Callback to invoke when foreground color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img, isError) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img, isError); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + image && (image.canvas = this); + callback && callback(image, false); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + + /** + * @private + */ + _createCanvasElement: function() { + var element = createCanvasElement(); + if (!element) { + throw CANVAS_INIT_ERROR; + } + if (!element.style) { + element.style = { }; + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + return element; + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions: function (options) { + var lowerCanvasEl = this.lowerCanvasEl; + this._setOptions(options); + + this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; + this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) { + return; + } + + lowerCanvasEl.width = this.width; + lowerCanvasEl.height = this.height; + + lowerCanvasEl.style.width = this.width + 'px'; + lowerCanvasEl.style.height = this.height + 'px'; + + this.viewportTransform = this.viewportTransform.slice(); + }, + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas: function (canvasEl) { + // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node + if (canvasEl && canvasEl.getContext) { + this.lowerCanvasEl = canvasEl; + } + else { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + } + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + this._originalCanvasStyle = this.lowerCanvasEl.style; + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width (in px) + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height (in px) + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @param {Number|String} value Value to set width to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value, options) { + return this.setDimensions({ width: value }, options); + }, + + /** + * Sets height of this canvas instance + * @param {Number|String} value Value to set height to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value, options) { + return this.setDimensions({ height: value }, options); + }, + + /** + * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) + * @param {Object} dimensions Object with width/height properties + * @param {Number|String} [dimensions.width] Width of canvas element + * @param {Number|String} [dimensions.height] Height of canvas element + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function (dimensions, options) { + var cssValue; + + options = options || {}; + + for (var prop in dimensions) { + cssValue = dimensions[prop]; + + if (!options.cssOnly) { + this._setBackstoreDimension(prop, dimensions[prop]); + cssValue += 'px'; + this.hasLostContext = true; + } + + if (!options.backstoreOnly) { + this._setCssDimension(prop, cssValue); + } + } + if (this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(); + } + this._initRetinaScaling(); + this.calcOffset(); + + if (!options.cssOnly) { + this.requestRenderAll(); + } + + return this; + }, + + /** + * Helper for setting width/height + * @private + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setBackstoreDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + this[prop] = value; + + return this; + }, + + /** + * Helper for setting css width/height + * @private + * @param {String} prop property (width|height) + * @param {String} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setCssDimension: function (prop, value) { + this.lowerCanvasEl.style[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl.style[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value; + } + + return this; + }, + + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return this.viewportTransform[0]; + }, + + /** + * Sets viewport transform of this canvas instance + * @param {Array} vpt the transform in the form of context.transform + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform: function (vpt) { + var activeObject = this._activeObject, + backgroundObject = this.backgroundImage, + overlayObject = this.overlayImage, + object, i, len; + this.viewportTransform = vpt; + for (i = 0, len = this._objects.length; i < len; i++) { + object = this._objects[i]; + object.group || object.setCoords(true); + } + if (activeObject) { + activeObject.setCoords(); + } + if (backgroundObject) { + backgroundObject.setCoords(true); + } + if (overlayObject) { + overlayObject.setCoords(true); + } + this.calcViewportBoundaries(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Sets zoom level of this canvas instance, the zoom centered around point + * meaning that following zoom to point with the same point will have the visual + * effect of the zoom originating from that point. The point won't move. + * It has nothing to do with canvas center or visual center of the viewport. + * @param {fabric.Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint: function (point, value) { + // TODO: just change the scale, preserve other transformations + var before = point, vpt = this.viewportTransform.slice(0); + point = transformPoint(point, invertTransform(this.viewportTransform)); + vpt[0] = value; + vpt[3] = value; + var after = transformPoint(point, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + this.zoomToPoint(new fabric.Point(0, 0), value); + return this; + }, + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {fabric.Point} point to move to + * @return {fabric.Canvas} instance + * @chainable true + */ + absolutePan: function (point) { + var vpt = this.viewportTransform.slice(0); + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); + }, + + /** + * Pans viewpoint relatively + * @param {fabric.Point} point (position vector) to move by + * @return {fabric.Canvas} instance + * @chainable true + */ + relativePan: function (point) { + return this.absolutePan(new fabric.Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + )); + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + this.fire('object:removed', { target: obj }); + obj.fire('removed'); + delete obj.canvas; + }, + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this.remove.apply(this, this.getObjects()); + this.backgroundImage = null; + this.overlayImage = null; + this.backgroundColor = ''; + this.overlayColor = ''; + if (this._hasITextHandlers) { + this.off('mouse:up', this._mouseUpITextHandler); + this._iTextInstances = null; + this._hasITextHandlers = false; + } + this.clearContext(this.contextContainer); + this.fire('canvas:cleared'); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Renders the canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._objects); + + return this; + }, + + /** + * Function created to be instance bound at initialization + * used in requestAnimationFrame rendering + * Let the fabricJS call it. If you call it manually you could have more + * animationFrame stacking on to of each other + * for an imperative rendering, use canvas.renderAll + * @private + * @return {fabric.Canvas} instance + * @chainable + */ + renderAndReset: function() { + this.isRendering = 0; + this.renderAll(); + }, + + /** + * Append a renderAll request to next animation frame. + * unless one is already in progress, in that case nothing is done + * a boolean flag will avoid appending more. + * @return {fabric.Canvas} instance + * @chainable + */ + requestRenderAll: function () { + if (!this.isRendering) { + this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound); + } + return this; + }, + + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries: function() { + var points = { }, width = this.width, height = this.height, + iVpt = invertTransform(this.viewportTransform); + points.tl = transformPoint({ x: 0, y: 0 }, iVpt); + points.br = transformPoint({ x: width, y: height }, iVpt); + points.tr = new fabric.Point(points.br.x, points.tl.y); + points.bl = new fabric.Point(points.tl.x, points.br.y); + this.vptCoords = points; + return points; + }, + + cancelRequestedRender: function() { + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + }, + + /** + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render + * @return {fabric.Canvas} instance + * @chainable + */ + renderCanvas: function(ctx, objects) { + var v = this.viewportTransform, path = this.clipPath; + this.cancelRequestedRender(); + this.calcViewportBoundaries(); + this.clearContext(ctx); + fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled); + this.fire('before:render', { ctx: ctx, }); + this._renderBackground(ctx); + + ctx.save(); + //apply viewport transform once for all rendering process + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._renderObjects(ctx, objects); + ctx.restore(); + if (!this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + if (path) { + path.canvas = this; + // needed to setup a couple of variables + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCanvas(ctx); + } + this._renderOverlay(ctx); + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + this.fire('after:render', { ctx: ctx, }); + }, + + /** + * Paint the cached clipPath on the lowerCanvasEl + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCanvas: function(ctx) { + var v = this.viewportTransform, path = this.clipPath; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4; + ctx.globalCompositeOperation = 'destination-in'; + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} objects to render + */ + _renderObjects: function(ctx, objects) { + var i, len; + for (i = 0, len = objects.length; i < len; ++i) { + objects[i] && objects[i].render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {string} property 'background' or 'overlay' + */ + _renderBackgroundOrOverlay: function(ctx, property) { + var fill = this[property + 'Color'], object = this[property + 'Image'], + v = this.viewportTransform, needsVpt = this[property + 'Vpt']; + if (!fill && !object) { + return; + } + if (fill) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(this.width, 0); + ctx.lineTo(this.width, this.height); + ctx.lineTo(0, this.height); + ctx.closePath(); + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx, this) + : fill; + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); + var m = fill.gradientTransform || fill.patternTransform; + m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + ctx.fill(); + ctx.restore(); + } + if (object) { + ctx.save(); + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + object.render(ctx); + ctx.restore(); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'background'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'overlay'); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.height / 2, + left: this.width / 2 + }; + }, + + /** + * Centers object horizontally in the canvas + * @param {fabric.Object} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + }, + + /** + * Centers object vertically in the canvas + * @param {fabric.Object} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + }, + + /** + * Centers object vertically and horizontally in the canvas + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function(object) { + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); + }, + + /** + * Centers object vertically and horizontally in the viewport + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObject: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, vpCenter); + }, + + /** + * Centers object horizontally in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectH: function(object) { + var vpCenter = this.getVpCenter(); + this._centerObject(object, new fabric.Point(vpCenter.x, object.getCenterPoint().y)); + return this; + }, + + /** + * Centers object Vertically in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectV: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, vpCenter.y)); + }, + + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {fabric.Point} vpCenter, viewport center + * @chainable + */ + getVpCenter: function() { + var center = this.getCenter(), + iVpt = invertTransform(this.viewportTransform); + return transformPoint({ x: center.left, y: center.top }, iVpt); + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Returns dataless JSON representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} json string + */ + toDatalessJSON: function (propertiesToInclude) { + return this.toDatalessObject(propertiesToInclude); + }, + + /** + * Returns object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function (propertiesToInclude) { + return this._toObjectMethod('toObject', propertiesToInclude); + }, + + /** + * Returns dataless object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function (propertiesToInclude) { + return this._toObjectMethod('toDatalessObject', propertiesToInclude); + }, + + /** + * @private + */ + _toObjectMethod: function (methodName, propertiesToInclude) { + + var clipPath = this.clipPath, data = { + version: fabric.version, + objects: this._toObjects(methodName, propertiesToInclude), + }; + if (clipPath && !clipPath.excludeFromExport) { + data.clipPath = this._toObject(this.clipPath, methodName, propertiesToInclude); + } + extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); + + fabric.util.populateWithProperties(this, data, propertiesToInclude); + + return data; + }, + + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this._objects.filter(function(object) { + return !object.excludeFromExport; + }).map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function(methodName, propertiesToInclude) { + var data = {}, bgImage = this.backgroundImage, overlayImage = this.overlayImage, + bgColor = this.backgroundColor, overlayColor = this.overlayColor; + + if (bgColor && bgColor.toObject) { + if (!bgColor.excludeFromExport) { + data.background = bgColor.toObject(propertiesToInclude); + } + } + else if (bgColor) { + data.background = bgColor; + } + + if (overlayColor && overlayColor.toObject) { + if (!overlayColor.excludeFromExport) { + data.overlay = overlayColor.toObject(propertiesToInclude); + } + } + else if (overlayColor) { + data.overlay = overlayColor; + } + + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject(bgImage, methodName, propertiesToInclude); + } + if (overlayImage && !overlayImage.excludeFromExport) { + data.overlayImage = this._toObject(overlayImage, methodName, propertiesToInclude); + } + + return data; + }, + + /* _TO_SVG_START_ */ + /** + * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, + * a zoomed canvas will then produce zoomed SVG output. + * @type Boolean + * @default + */ + svgViewportTransformation: true, + + /** + * Returns SVG representation of canvas + * @function + * @param {Object} [options] Options object for SVG output + * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included + * @param {Object} [options.viewBox] SVG viewbox object + * @param {Number} [options.viewBox.x] x-coordinate of viewbox + * @param {Number} [options.viewBox.y] y-coordinate of viewbox + * @param {Number} [options.viewBox.width] Width of viewbox + * @param {Number} [options.viewBox.height] Height of viewbox + * @param {String} [options.encoding=UTF-8] Encoding of SVG output + * @param {String} [options.width] desired width of svg with or without units + * @param {String} [options.height] desired height of svg with or without units + * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. + * @return {String} SVG string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} + * @example Normal SVG output + * var svg = canvas.toSVG(); + * @example SVG output without preamble (without <?xml ../>) + * var svg = canvas.toSVG({suppressPreamble: true}); + * @example SVG output with viewBox attribute + * var svg = canvas.toSVG({ + * viewBox: { + * x: 100, + * y: 100, + * width: 200, + * height: 300 + * } + * }); + * @example SVG output with different encoding (default: UTF-8) + * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); + * @example Modify SVG output with reviver function + * var svg = canvas.toSVG(null, function(svg) { + * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); + * }); + */ + toSVG: function(options, reviver) { + options || (options = { }); + options.reviver = reviver; + var markup = []; + + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'background'); + this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); + this._setSVGObjects(markup, reviver); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'overlay'); + this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { + var width = options.width || this.width, + height = options.height || this.height, + vpt, viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + if (options.viewBox) { + viewBox = 'viewBox="' + + options.viewBox.x + ' ' + + options.viewBox.y + ' ' + + options.viewBox.width + ' ' + + options.viewBox.height + '" '; + } + else { + if (this.svgViewportTransformation) { + vpt = this.viewportTransform; + viewBox = 'viewBox="' + + toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + '" '; + } + } + + markup.push( + '\n', + 'Created with Fabric.js ', fabric.version, '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + this.createSVGClipPathMarkup(options), + '\n' + ); + }, + + createSVGClipPathMarkup: function(options) { + var clipPath = this.clipPath; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + return '\n' + + this.clipPath.toClipPathSVG(options.reviver) + + '\n'; + } + return ''; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup: function() { + var _this = this, + markup = ['background', 'overlay'].map(function(prop) { + var fill = _this[prop + 'Color']; + if (fill && fill.toLive) { + var shouldTransform = _this[prop + 'Vpt'], vpt = _this.viewportTransform, + object = { + width: _this.width / (shouldTransform ? vpt[0] : 1), + height: _this.height / (shouldTransform ? vpt[3] : 1) + }; + return fill.toSVG( + object, + { additionalTransform: shouldTransform ? fabric.util.matrixToSVG(vpt) : '' } + ); + } + }); + return markup.join(''); + }, + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function() { + var markup = '', fontList = { }, obj, fontFamily, + style, row, rowIndex, _char, charIndex, i, len, + fontPaths = fabric.fontPaths, objects = []; + + this._objects.forEach(function add(object) { + objects.push(object); + if (object._objects) { + object._objects.forEach(add); + } + }); + + for (i = 0, len = objects.length; i < len; i++) { + obj = objects[i]; + fontFamily = obj.fontFamily; + if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { + continue; + } + fontList[fontFamily] = true; + if (!obj.styles) { + continue; + } + style = obj.styles; + for (rowIndex in style) { + row = style[rowIndex]; + for (charIndex in row) { + _char = row[charIndex]; + fontFamily = _char.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; + } + } + } + } + + for (var j in fontList) { + markup += [ + '\t\t@font-face {\n', + '\t\t\tfont-family: \'', j, '\';\n', + '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', + '\t\t}\n' + ].join(''); + } + + if (markup) { + markup = [ + '\t\n' + ].join(''); + } + + return markup; + }, + + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { + var instance, i, len, objects = this._objects; + for (i = 0, len = objects.length; i < len; i++) { + instance = objects[i]; + if (instance.excludeFromExport) { + continue; + } + this._setSVGObject(markup, instance, reviver); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + markup.push(instance.toSVG(reviver)); + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property, reviver) { + if (this[property] && !this[property].excludeFromExport && this[property].toSVG) { + markup.push(this[property].toSVG(reviver)); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + var filler = this[property + 'Color'], vpt = this.viewportTransform, finalWidth = this.width, + finalHeight = this.height; + if (!filler) { + return; + } + if (filler.toLive) { + var repeat = filler.repeat, iVpt = fabric.util.invertTransform(vpt), shouldInvert = this[property + 'Vpt'], + additionalTransform = shouldInvert ? fabric.util.matrixToSVG(iVpt) : ''; + markup.push( + '\n' + ); + } + else { + markup.push( + '\n' + ); + } + }, + /* _TO_SVG_END_ */ + + /** + * Moves an object or the objects of a multiple selection + * to the bottom of the stack of drawn objects + * @param {fabric.Object} object Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.unshift(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.unshift(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or the objects of a multiple selection + * to the top of the stack of drawn objects + * @param {fabric.Object} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.push(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.push(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or a selection down in stack of drawn objects + * An optional parameter, intersecting allows to move the object in behind + * the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx > 0 + objsMoved) { + newIdx = idx - 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== 0) { + // if object is not on the bottom of stack + newIdx = this._findNewLowerIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx, i; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (i = idx - 1; i >= 0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + + /** + * Moves an object or a selection up in stack of drawn objects + * An optional parameter, intersecting allows to move the object in front + * of the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx < this._objects.length - 1 - objsMoved) { + newIdx = idx + 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== this._objects.length - 1) { + // if object is not on top of stack (last item in an array) + newIdx = this._findNewUpperIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx, i, len; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (i = idx + 1, len = this._objects.length; i < len; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx + 1; + } + + return newIdx; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {fabric.Object} object Object to send + * @param {Number} index Position to move to + * @return {fabric.Canvas} thisArg + * @chainable + */ + moveTo: function (object, index) { + removeFromArray(this._objects, object); + this._objects.splice(index, 0, object); + return this.renderOnAddRemove && this.requestRenderAll(); + }, + + /** + * Clears a canvas element and dispose objects + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + // cancel eventually ongoing renders + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + this.forEachObject(function(object) { + object.dispose && object.dispose(); + }); + this._objects = []; + if (this.backgroundImage && this.backgroundImage.dispose) { + this.backgroundImage.dispose(); + } + this.backgroundImage = null; + if (this.overlayImage && this.overlayImage.dispose) { + this.overlayImage.dispose(); + } + this.overlayImage = null; + this._iTextInstances = null; + this.contextContainer = null; + // restore canvas style + this.lowerCanvasEl.classList.remove('lower-canvas'); + this.lowerCanvasEl.style = this._originalCanvasStyle; + delete this._originalCanvasStyle; + // restore canvas size to original size in case retina scaling was applied + this.lowerCanvasEl.setAttribute('width', this.width); + this.lowerCanvasEl.setAttribute('height', this.height); + fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); + this.lowerCanvasEl = undefined; + return this; + }, + + /** + * Returns a string representation of an instance + * @return {String} string representation of an instance + */ + toString: function () { + return '#'; + } + }); + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + extend(fabric.StaticCanvas.prototype, fabric.Collection); + extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); + + extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { + + /** + * @static + * @type String + * @default + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @param {String} methodName Method to check support for; + * Could be one of "setLineDash" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = createCanvasElement(); + + if (!el || !el.getContext) { + return null; + } + + var ctx = el.getContext('2d'); + if (!ctx) { + return null; + } + + switch (methodName) { + + case 'setLineDash': + return typeof ctx.setLineDash !== 'undefined'; + + default: + return null; + } + } + }); + + /** + * Returns Object representation of canvas + * this alias is provided because if you call JSON.stringify on an instance, + * the toJSON object will be invoked if it exists. + * Having a toJSON method means you can do JSON.stringify(myCanvas) + * @function + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON compatible object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} + * @example JSON without additional properties + * var json = canvas.toJSON(); + * @example JSON with additional properties included + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); + * @example JSON without default values + * canvas.includeDefaultValues = false; + * var json = canvas.toJSON(); + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + + if (fabric.isLikelyNode) { + fabric.StaticCanvas.prototype.createPNGStream = function() { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createPNGStream(); + }; + fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createJPEGStream(opts); + }; + } +})(); + + +/** + * BaseBrush class + * @class fabric.BaseBrush + * @see {@link http://fabricjs.com/freedrawing|Freedrawing demo} + */ +fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ { + + /** + * Color of a brush + * @type String + * @default + */ + color: 'rgb(0, 0, 0)', + + /** + * Width of a brush, has to be a Number, no string literals + * @type Number + * @default + */ + width: 1, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), + * "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Line endings style of a brush (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'round', + + /** + * Corner style of a brush (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'round', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of a brush's + * @type Number + * @default + */ + strokeMiterLimit: 10, + + /** + * Stroke Dash Array. + * @type Array + * @default + */ + strokeDashArray: null, + + /** + * When `true`, the free drawing is limited to the whiteboard size. Default to false. + * @type Boolean + * @default false + */ + + limitedToCanvasSize: false, + + + /** + * Sets brush styles + * @private + */ + _setBrushStyles: function() { + var ctx = this.canvas.contextTop; + ctx.strokeStyle = this.color; + ctx.lineWidth = this.width; + ctx.lineCap = this.strokeLineCap; + ctx.miterLimit = this.strokeMiterLimit; + ctx.lineJoin = this.strokeLineJoin; + ctx.setLineDash(this.strokeDashArray || []); + }, + + /** + * Sets the transformation on given context + * @param {RenderingContext2d} ctx context to render on + * @private + */ + _saveAndTransform: function(ctx) { + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + }, + + /** + * Sets brush shadow styles + * @private + */ + _setShadow: function() { + if (!this.shadow) { + return; + } + + var canvas = this.canvas, + shadow = this.shadow, + ctx = canvas.contextTop, + zoom = canvas.getZoom(); + if (canvas && canvas._isRetinaScaling()) { + zoom *= fabric.devicePixelRatio; + } + + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * zoom; + ctx.shadowOffsetX = shadow.offsetX * zoom; + ctx.shadowOffsetY = shadow.offsetY * zoom; + }, + + needsFullRender: function() { + var color = new fabric.Color(this.color); + return color.getAlpha() < 1 || !!this.shadow; + }, + + /** + * Removes brush shadow styles + * @private + */ + _resetShadow: function() { + var ctx = this.canvas.contextTop; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * Check is pointer is outside canvas boundaries + * @param {Object} pointer + * @private + */ + _isOutSideCanvas: function(pointer) { + return pointer.x < 0 || pointer.x > this.canvas.getWidth() || pointer.y < 0 || pointer.y > this.canvas.getHeight(); + } +}); + + +(function() { + /** + * PencilBrush class + * @class fabric.PencilBrush + * @extends fabric.BaseBrush + */ + fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { + + /** + * Discard points that are less than `decimate` pixel distant from each other + * @type Number + * @default 0.4 + */ + decimate: 0.4, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.PencilBrush} Instance of a pencil brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this._points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + _drawSegment: function (ctx, p1, p2) { + var midPoint = p1.midPointFrom(p2); + ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); + return midPoint; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this._prepareForDrawing(pointer); + // capture coordinates immediately + // this allows to draw dots (when movement never occurs) + this._captureDrawingPath(pointer); + this._render(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + if (this._captureDrawingPath(pointer) && this._points.length > 1) { + if (this.needsFullRender()) { + // redraw curve + // clear top canvas + this.canvas.clearContext(this.canvas.contextTop); + this._render(); + } + else { + var points = this._points, length = points.length, ctx = this.canvas.contextTop; + // draw the curve update + this._saveAndTransform(ctx); + if (this.oldEnd) { + ctx.beginPath(); + ctx.moveTo(this.oldEnd.x, this.oldEnd.y); + } + this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); + ctx.stroke(); + ctx.restore(); + } + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function(options) { + if (!this.canvas._isMainEvent(options.e)) { + return true; + } + this.oldEnd = undefined; + this._finalizeAndAddPath(); + return false; + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _prepareForDrawing: function(pointer) { + + var p = new fabric.Point(pointer.x, pointer.y); + + this._reset(); + this._addPoint(p); + this.canvas.contextTop.moveTo(p.x, p.y); + }, + + /** + * @private + * @param {fabric.Point} point Point to be added to points array + */ + _addPoint: function(point) { + if (this._points.length > 1 && point.eq(this._points[this._points.length - 1])) { + return false; + } + this._points.push(point); + return true; + }, + + /** + * Clear points array and set contextTop canvas style. + * @private + */ + _reset: function() { + this._points = []; + this._setBrushStyles(); + this._setShadow(); + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _captureDrawingPath: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + return this._addPoint(pointerPoint); + }, + + /** + * Draw a smooth path on the topCanvas using quadraticCurveTo + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + p1 = this._points[0], + p2 = this._points[1]; + + this._saveAndTransform(ctx); + ctx.beginPath(); + //if we only have 2 points in the path and they are the same + //it means that the user only clicked the canvas without moving the mouse + //then we should be drawing a dot. A path isn't drawn between two identical dots + //that's why we set them apart a bit + if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { + var width = this.width / 1000; + p1 = new fabric.Point(p1.x, p1.y); + p2 = new fabric.Point(p2.x, p2.y); + p1.x -= width; + p2.x += width; + } + ctx.moveTo(p1.x, p1.y); + + for (i = 1, len = this._points.length; i < len; i++) { + // we pick the point between pi + 1 & pi + 2 as the + // end point and p1 as our control point. + this._drawSegment(ctx, p1, p2); + p1 = this._points[i]; + p2 = this._points[i + 1]; + } + // Draw last line as a straight line while + // we wait for the next point to be able to calculate + // the bezier control point + ctx.lineTo(p1.x, p1.y); + ctx.stroke(); + ctx.restore(); + }, + + /** + * Converts points to SVG path + * @param {Array} points Array of points + * @return {(string|number)[][]} SVG path commands + */ + convertPointsToSVGPath: function (points) { + var correction = this.width / 1000; + return fabric.util.getSmoothPathFromPoints(points, correction); + }, + + /** + * @private + * @param {(string|number)[][]} pathData SVG path commands + * @returns {boolean} + */ + _isEmptySVGPath: function (pathData) { + var pathString = fabric.util.joinPath(pathData); + return pathString === 'M 0 0 Q 0 0 0 0 L 0 0'; + }, + + /** + * Creates fabric.Path object to add on canvas + * @param {(string|number)[][]} pathData Path data + * @return {fabric.Path} Path to add on canvas + */ + createPath: function(pathData) { + var path = new fabric.Path(pathData, { + fill: null, + stroke: this.color, + strokeWidth: this.width, + strokeLineCap: this.strokeLineCap, + strokeMiterLimit: this.strokeMiterLimit, + strokeLineJoin: this.strokeLineJoin, + strokeDashArray: this.strokeDashArray, + }); + if (this.shadow) { + this.shadow.affectStroke = true; + path.shadow = new fabric.Shadow(this.shadow); + } + + return path; + }, + + /** + * Decimate points array with the decimate value + */ + decimatePoints: function(points, distance) { + if (points.length <= 2) { + return points; + } + var zoom = this.canvas.getZoom(), adjustedDistance = Math.pow(distance / zoom, 2), + i, l = points.length - 1, lastPoint = points[0], newPoints = [lastPoint], + cDistance; + for (i = 1; i < l - 1; i++) { + cDistance = Math.pow(lastPoint.x - points[i].x, 2) + Math.pow(lastPoint.y - points[i].y, 2); + if (cDistance >= adjustedDistance) { + lastPoint = points[i]; + newPoints.push(lastPoint); + } + } + /** + * Add the last point from the original line to the end of the array. + * This ensures decimate doesn't delete the last point on the line, and ensures the line is > 1 point. + */ + newPoints.push(points[l]); + return newPoints; + }, + + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. + */ + _finalizeAndAddPath: function() { + var ctx = this.canvas.contextTop; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points); + if (this._isEmptySVGPath(pathData)) { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.requestRenderAll(); + return; + } + + var path = this.createPath(pathData); + this.canvas.clearContext(this.canvas.contextTop); + this.canvas.fire('before:path:created', { path: path }); + this.canvas.add(path); + this.canvas.requestRenderAll(); + path.setCoords(); + this._resetShadow(); + + + // fire event 'path' created + this.canvas.fire('path:created', { path: path }); + } + }); +})(); + + +/** + * CircleBrush class + * @class fabric.CircleBrush + */ +fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { + + /** + * Width of a brush + * @type Number + * @default + */ + width: 10, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.CircleBrush} Instance of a circle brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + drawDot: function(pointer) { + var point = this.addPoint(pointer), + ctx = this.canvas.contextTop; + this._saveAndTransform(ctx); + this.dot(ctx, point); + ctx.restore(); + }, + + dot: function(ctx, point) { + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + }, + + /** + * Invoked on mouse down + */ + onMouseDown: function(pointer) { + this.points.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + this.drawDot(pointer); + }, + + /** + * Render the full state of the brush + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + points = this.points; + this._saveAndTransform(ctx); + for (i = 0, len = points.length; i < len; i++) { + this.dot(ctx, points[i]); + } + ctx.restore(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + if (this.needsFullRender()) { + this.canvas.clearContext(this.canvas.contextTop); + this.addPoint(pointer); + this._render(); + } + else { + this.drawDot(pointer); + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove, i, len; + this.canvas.renderOnAddRemove = false; + + var circles = []; + + for (i = 0, len = this.points.length; i < len; i++) { + var point = this.points[i], + circle = new fabric.Circle({ + radius: point.radius, + left: point.x, + top: point.y, + originX: 'center', + originY: 'center', + fill: point.fill + }); + + this.shadow && (circle.shadow = new fabric.Shadow(this.shadow)); + + circles.push(circle); + } + var group = new fabric.Group(circles); + group.canvas = this.canvas; + + this.canvas.fire('before:path:created', { path: group }); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @param {Object} pointer + * @return {fabric.Point} Just added pointer point + */ + addPoint: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y), + + circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2, + + circleColor = new fabric.Color(this.color) + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); + + pointerPoint.radius = circleRadius; + pointerPoint.fill = circleColor; + + this.points.push(pointerPoint); + + return pointerPoint; + } +}); + + +/** + * SprayBrush class + * @class fabric.SprayBrush + */ +fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { + + /** + * Width of a spray + * @type Number + * @default + */ + width: 10, + + /** + * Density of a spray (number of dots per chunk) + * @type Number + * @default + */ + density: 20, + + /** + * Width of spray dots + * @type Number + * @default + */ + dotWidth: 1, + + /** + * Width variance of spray dots + * @type Number + * @default + */ + dotWidthVariance: 1, + + /** + * Whether opacity of a dot should be random + * @type Boolean + * @default + */ + randomOpacity: false, + + /** + * Whether overlapping dots (rectangles) should be removed (for performance reasons) + * @type Boolean + * @default + */ + optimizeOverlapping: true, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.SprayBrush} Instance of a spray brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.sprayChunks = []; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer) { + this.sprayChunks.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + var rects = []; + + for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + var sprayChunk = this.sprayChunks[i]; + + for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) { + + var rect = new fabric.Rect({ + width: sprayChunk[j].width, + height: sprayChunk[j].width, + left: sprayChunk[j].x + 1, + top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', + fill: this.color + }); + rects.push(rect); + } + } + + if (this.optimizeOverlapping) { + rects = this._getOptimizedRects(rects); + } + + var group = new fabric.Group(rects); + this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); + this.canvas.fire('before:path:created', { path: group }); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @private + * @param {Array} rects + */ + _getOptimizedRects: function(rects) { + + // avoid creating duplicate rects at the same coordinates + var uniqueRects = { }, key, i, len; + + for (i = 0, len = rects.length; i < len; i++) { + key = rects[i].left + '' + rects[i].top; + if (!uniqueRects[key]) { + uniqueRects[key] = rects[i]; + } + } + var uniqueRectsArray = []; + for (key in uniqueRects) { + uniqueRectsArray.push(uniqueRects[key]); + } + + return uniqueRectsArray; + }, + + /** + * Render new chunk of spray brush + */ + render: function(sprayChunk) { + var ctx = this.canvas.contextTop, i, len; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, len = sprayChunk.length; i < len; i++) { + var point = sprayChunk[i]; + if (typeof point.opacity !== 'undefined') { + ctx.globalAlpha = point.opacity; + } + ctx.fillRect(point.x, point.y, point.width, point.width); + } + ctx.restore(); + }, + + /** + * Render all spray chunks + */ + _render: function() { + var ctx = this.canvas.contextTop, i, ilen; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + this.render(this.sprayChunks[i]); + } + ctx.restore(); + }, + + /** + * @param {Object} pointer + */ + addSprayChunk: function(pointer) { + this.sprayChunkPoints = []; + + var x, y, width, radius = this.width / 2, i; + + for (i = 0; i < this.density; i++) { + + x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); + y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); + + if (this.dotWidthVariance) { + width = fabric.util.getRandomInt( + // bottom clamp width to 1 + Math.max(1, this.dotWidth - this.dotWidthVariance), + this.dotWidth + this.dotWidthVariance); + } + else { + width = this.dotWidth; + } + + var point = new fabric.Point(x, y); + point.width = width; + + if (this.randomOpacity) { + point.opacity = fabric.util.getRandomInt(0, 100) / 100; + } + + this.sprayChunkPoints.push(point); + } + + this.sprayChunks.push(this.sprayChunkPoints); + } +}); + + +/** + * PatternBrush class + * @class fabric.PatternBrush + * @extends fabric.BaseBrush + */ +fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { + + getPatternSrc: function() { + + var dotWidth = 20, + dotDistance = 5, + patternCanvas = fabric.util.createCanvasElement(), + patternCtx = patternCanvas.getContext('2d'); + + patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; + + patternCtx.fillStyle = this.color; + patternCtx.beginPath(); + patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); + patternCtx.closePath(); + patternCtx.fill(); + + return patternCanvas; + }, + + getPatternSrcFunction: function() { + return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); + }, + + /** + * Creates "pattern" instance property + */ + getPattern: function() { + return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); + }, + + /** + * Sets brush styles + */ + _setBrushStyles: function() { + this.callSuper('_setBrushStyles'); + this.canvas.contextTop.strokeStyle = this.getPattern(); + }, + + /** + * Creates path + */ + createPath: function(pathData) { + var path = this.callSuper('createPath', pathData), + topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); + + path.stroke = new fabric.Pattern({ + source: this.source || this.getPatternSrcFunction(), + offsetX: -topLeft.x, + offsetY: -topLeft.y + }); + return path; + } +}); + + +(function() { + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + isTouchEvent = fabric.util.isTouchEvent; + + /** + * Canvas class + * @class fabric.Canvas + * @extends fabric.StaticCanvas + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition + * + * @fires object:modified at the end of a transform or any change when statefull is true + * @fires object:rotating while an object is being rotated from the control + * @fires object:scaling while an object is being scaled by controls + * @fires object:moving while an object is being dragged + * @fires object:skewing while an object is being skewed from the controls + * + * @fires before:transform before a transform is is started + * @fires before:selection:cleared + * @fires selection:cleared + * @fires selection:updated + * @fires selection:created + * + * @fires path:created after a drawing operation ends and the path is added + * @fires mouse:down + * @fires mouse:move + * @fires mouse:up + * @fires mouse:down:before on mouse down, before the inner fabric logic runs + * @fires mouse:move:before on mouse move, before the inner fabric logic runs + * @fires mouse:up:before on mouse up, before the inner fabric logic runs + * @fires mouse:over + * @fires mouse:out + * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + * @fires after:render at the end of the render process, receives the context in the callback + * @fires before:render at start the render process, receives the context in the callback + * + * the following events are deprecated: + * @fires object:rotated at the end of a rotation transform + * @fires object:scaled at the end of a scale transform + * @fires object:moved at the end of translation transform + * @fires object:skewed at the end of a skew transform + */ + fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + }, + + /** + * When true, objects can be transformed by one side (unproportionally) + * when dragged on the corners that normally would not do that. + * @type Boolean + * @default + * @since fabric 4.0 // changed name and default value + */ + uniformScaling: true, + + /** + * Indicates which key switches uniform scaling. + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * totally wrong named. this sounds like `uniform scaling` + * if Canvas.uniformScaling is true, pressing this will set it to false + * and viceversa. + * @since 1.6.2 + * @type String + * @default + */ + uniScaleKey: 'shiftKey', + + /** + * When true, objects use center point as the origin of scale transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, objects use center point as the origin of rotate transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: false, + + /** + * Indicates which key enable centered Transform + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + centeredKey: 'altKey', + + /** + * Indicates which key enable alternate action on corner + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + altActionKey: 'shiftKey', + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @type Boolean + * @default + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @type Boolean + * @default + */ + selection: true, + + /** + * Indicates which key or keys enable multiple click selection + * Pass value as a string or array of strings + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or empty or containing any other string that is not a modifier key + * feature is disabled. + * @since 1.6.2 + * @type String|Array + * @default + */ + selectionKey: 'shiftKey', + + /** + * Indicates which key enable alternative selection + * in case of target overlapping with active object + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * For a series of reason that come from the general expectations on how + * things should work, this feature works only for preserveObjectStacking true. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * @since 1.6.5 + * @type null|String + * @default + */ + altSelectionKey: null, + + /** + * Color of selection + * @type String + * @default + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Default dash array pattern + * If not empty the selection border is dashed + * @type Array + */ + selectionDashArray: [], + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @type String + * @default + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @type Number + * @default + */ + selectionLineWidth: 1, + + /** + * Select only shapes that are fully contained in the dragged selection rectangle. + * @type Boolean + * @default + */ + selectionFullyContained: false, + + /** + * Default cursor value used when hovering over an object on canvas + * @type String + * @default + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @type String + * @default + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @type String + * @default + */ + defaultCursor: 'default', + + /** + * Cursor value used during free drawing + * @type String + * @default + */ + freeDrawingCursor: 'crosshair', + + /** + * Cursor value used for rotation point + * @type String + * @default + */ + rotationCursor: 'crosshair', + + /** + * Cursor value used for disabled elements ( corners with disabled action ) + * @type String + * @since 2.0.0 + * @default + */ + notAllowedCursor: 'not-allowed', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @type String + * @default + */ + containerClass: 'canvas-container', + + /** + * When true, object detection happens on per-pixel basis rather than on per-bounding-box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * Number of pixels around target pixel to tolerate (consider active) during object detection + * @type Number + * @default + */ + targetFindTolerance: 0, + + /** + * When true, target detection is skipped. Target detection will return always undefined. + * click selection won't work anymore, events will fire with no targets. + * if something is selected before setting it to true, it will be deselected at the first click. + * area selection will still work. check the `selection` property too. + * if you deactivate both, you should look into staticCanvas. + * @type Boolean + * @default + */ + skipTargetFind: false, + + /** + * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. + * After mousedown, mousemove creates a shape, + * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. + * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} + * @type Boolean + * @default + */ + isDrawingMode: false, + + /** + * Indicates whether objects should remain in current stack position when selected. + * When false objects are brought to top and rendered as part of the selection group + * @type Boolean + * @default + */ + preserveObjectStacking: false, + + /** + * Indicates the angle that an object will lock to while rotating. + * @type Number + * @since 1.6.7 + * @default + */ + snapAngle: 0, + + /** + * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. + * When `null`, the snapThreshold will default to the snapAngle. + * @type null|Number + * @since 1.6.7 + * @default + */ + snapThreshold: null, + + /** + * Indicates if the right click on canvas can output the context menu or not + * @type Boolean + * @since 1.6.5 + * @default + */ + stopContextMenu: false, + + /** + * Indicates if the canvas can fire right click events + * @type Boolean + * @since 1.6.5 + * @default + */ + fireRightClick: false, + + /** + * Indicates if the canvas can fire middle click events + * @type Boolean + * @since 1.7.8 + * @default + */ + fireMiddleClick: false, + + /** + * Keep track of the subTargets for Mouse Events + * @type fabric.Object[] + */ + targets: [], + + /** + * Keep track of the hovered target + * @type fabric.Object + * @private + */ + _hoveredTarget: null, + + /** + * hold the list of nested targets hovered + * @type fabric.Object[] + * @private + */ + _hoveredTargets: [], + + /** + * @private + */ + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEventListeners(); + + this._initRetinaScaling(); + + this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + + this.calcOffset(); + }, + + /** + * Divides objects in two groups, one to render immediately + * and one to render as activeGroup. + * @return {Array} objects to render immediately and pushes the other in the activeGroup. + */ + _chooseObjectsToRender: function() { + var activeObjects = this.getActiveObjects(), + object, objsToRender, activeGroupObjects; + + if (activeObjects.length > 0 && !this.preserveObjectStacking) { + objsToRender = []; + activeGroupObjects = []; + for (var i = 0, length = this._objects.length; i < length; i++) { + object = this._objects[i]; + if (activeObjects.indexOf(object) === -1 ) { + objsToRender.push(object); + } + else { + activeGroupObjects.push(object); + } + } + if (activeObjects.length > 1) { + this._activeObject._objects = activeGroupObjects; + } + objsToRender.push.apply(objsToRender, activeGroupObjects); + } + else { + objsToRender = this._objects; + } + return objsToRender; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { + this.clearContext(this.contextTop); + this.contextTopDirty = false; + } + if (this.hasLostContext) { + this.renderTopLayer(this.contextTop); + } + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + return this; + }, + + renderTopLayer: function(ctx) { + ctx.save(); + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._render(); + this.contextTopDirty = true; + } + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(ctx); + this.contextTopDirty = true; + } + ctx.restore(); + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + var ctx = this.contextTop; + this.clearContext(ctx); + this.renderTopLayer(ctx); + this.fire('after:render'); + return this; + }, + + /** + * @private + */ + _normalizePointer: function (object, pointer) { + var m = object.calcTransformMatrix(), + invertedM = fabric.util.invertTransform(m), + vptPointer = this.restorePointerVpt(pointer); + return fabric.util.transformPoint(vptPointer, invertedM); + }, + + /** + * Returns true if object is transparent at a certain location + * @param {fabric.Object} target Object to check + * @param {Number} x Left coordinate + * @param {Number} y Top coordinate + * @return {Boolean} + */ + isTargetTransparent: function (target, x, y) { + // in case the target is the activeObject, we cannot execute this optimization + // because we need to draw controls too. + if (target.shouldCache() && target._cacheCanvas && target !== this._activeObject) { + var normalizedPointer = this._normalizePointer(target, {x: x, y: y}), + targetRelativeX = Math.max(target.cacheTranslationX + (normalizedPointer.x * target.zoomX), 0), + targetRelativeY = Math.max(target.cacheTranslationY + (normalizedPointer.y * target.zoomY), 0); + + var isTransparent = fabric.util.isTransparent( + target._cacheContext, Math.round(targetRelativeX), Math.round(targetRelativeY), this.targetFindTolerance); + + return isTransparent; + } + + var ctx = this.contextCache, + originalColor = target.selectionBackgroundColor, v = this.viewportTransform; + + target.selectionBackgroundColor = ''; + + this.clearContext(ctx); + + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + target.render(ctx); + ctx.restore(); + + target.selectionBackgroundColor = originalColor; + + var isTransparent = fabric.util.isTransparent( + ctx, x, y, this.targetFindTolerance); + + return isTransparent; + }, + + /** + * takes an event and determines if selection key has been pressed + * @private + * @param {Event} e Event object + */ + _isSelectionKeyPressed: function(e) { + var selectionKeyPressed = false; + + if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { + selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); + } + else { + selectionKeyPressed = e[this.selectionKey]; + } + + return selectionKeyPressed; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldClearSelection: function (e, target) { + var activeObjects = this.getActiveObjects(), + activeObject = this._activeObject; + + return ( + !target + || + (target && + activeObject && + activeObjects.length > 1 && + activeObjects.indexOf(target) === -1 && + activeObject !== target && + !this._isSelectionKeyPressed(e)) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * centeredScaling from object can't override centeredScaling from canvas. + * this should be fixed, since object setting should take precedence over canvas. + * also this should be something that will be migrated in the control properties. + * as ability to define the origin of the transformation that the control provide. + * @private + * @param {fabric.Object} target + * @param {String} action + * @param {Boolean} altKey + */ + _shouldCenterTransform: function (target, action, altKey) { + if (!target) { + return; + } + + var centerTransform; + + if (action === 'scale' || action === 'scaleX' || action === 'scaleY' || action === 'resizing') { + centerTransform = this.centeredScaling || target.centeredScaling; + } + else if (action === 'rotate') { + centerTransform = this.centeredRotation || target.centeredRotation; + } + + return centerTransform ? !altKey : altKey; + }, + + /** + * should disappear before release 4.0 + * @private + */ + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; + + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } + + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + return origin; + }, + + /** + * @private + * @param {Boolean} alreadySelected true if target is already selected + * @param {String} corner a string representing the corner ml, mr, tl ... + * @param {Event} e Event object + * @param {fabric.Object} [target] inserted back to help overriding. Unused + */ + _getActionFromCorner: function(alreadySelected, corner, e, target) { + if (!corner || !alreadySelected) { + return 'drag'; + } + var control = target.controls[corner]; + return control.getActionName(e, control, target); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target, alreadySelected) { + if (!target) { + return; + } + + var pointer = this.getPointer(e), corner = target.__corner, + control = target.controls[corner], + actionHandler = (alreadySelected && corner) ? + control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, + action = this._getActionFromCorner(alreadySelected, corner, e, target), + origin = this._getOriginFromCorner(target, corner), + altKey = e[this.centeredKey], + transform = { + target: target, + action: action, + actionHandler: actionHandler, + corner: corner, + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + // used by transation + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + lastX: pointer.x, + lastY: pointer.y, + // unsure they are useful anymore. + // left: target.left, + // top: target.top, + theta: degreesToRadians(target.angle), + // end of unsure + width: target.width * target.scaleX, + shiftKey: e.shiftKey, + altKey: altKey, + original: fabric.util.saveObjectTransform(target), + }; + + if (this._shouldCenterTransform(target, action, altKey)) { + transform.originX = 'center'; + transform.originY = 'center'; + } + transform.original.originX = origin.x; + transform.original.originY = origin.y; + this._currentTransform = transform; + this._beforeTransform(e); + }, + + /** + * Set the cursor type of the canvas element + * @param {String} value Cursor type of the canvas element. + * @see http://www.w3.org/TR/css3-ui/#cursor + */ + setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx to draw the selection on + */ + _drawSelection: function (ctx) { + var selector = this._groupSelector, + viewportStart = new fabric.Point(selector.ex, selector.ey), + start = fabric.util.transformPoint(viewportStart, this.viewportTransform), + viewportExtent = new fabric.Point(selector.ex + selector.left, selector.ey + selector.top), + extent = fabric.util.transformPoint(viewportExtent, this.viewportTransform), + minX = Math.min(start.x, extent.x), + minY = Math.min(start.y, extent.y), + maxX = Math.max(start.x, extent.x), + maxY = Math.max(start.y, extent.y), + strokeOffset = this.selectionLineWidth / 2; + + if (this.selectionColor) { + ctx.fillStyle = this.selectionColor; + ctx.fillRect(minX, minY, maxX - minX, maxY - minY); + } + + if (!this.selectionLineWidth || !this.selectionBorderColor) { + return; + } + ctx.lineWidth = this.selectionLineWidth; + ctx.strokeStyle = this.selectionBorderColor; + + minX += strokeOffset; + minY += strokeOffset; + maxX -= strokeOffset; + maxY -= strokeOffset; + // selection border + fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); + ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); + }, + + /** + * Method that determines what object we are clicking on + * the skipGroup parameter is for internal use, is needed for shift+click action + * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target + * or the outside part of the corner. + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through + * @return {fabric.Object} the target found + */ + findTarget: function (e, skipGroup) { + if (this.skipTargetFind) { + return; + } + + var ignoreZoom = true, + pointer = this.getPointer(e, ignoreZoom), + activeObject = this._activeObject, + aObjects = this.getActiveObjects(), + activeTarget, activeTargetSubs, + isTouch = isTouchEvent(e), + shouldLookForActive = (aObjects.length > 1 && !skipGroup) || aObjects.length === 1; + + // first check current group (if one exists) + // active group does not check sub targets like normal groups. + // if active group just exits. + this.targets = []; + + // if we hit the corner of an activeObject, let's return that. + if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { + return activeObject; + } + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + return activeObject; + } + if (aObjects.length === 1 && + activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (!this.preserveObjectStacking) { + return activeObject; + } + else { + activeTarget = activeObject; + activeTargetSubs = this.targets; + this.targets = []; + } + } + var target = this._searchPossibleTargets(this._objects, pointer); + if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { + target = activeTarget; + this.targets = activeTargetSubs; + } + return target; + }, + + /** + * Checks point is inside the object. + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @param {fabric.Object} obj Object to test against + * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. + * @return {Boolean} true if point is contained within an area of given object + * @private + */ + _checkTarget: function(pointer, obj, globalPointer) { + if (obj && + obj.visible && + obj.evented && + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + obj.containsPoint(pointer) + ) { + if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { + var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); + if (!isTransparent) { + return true; + } + } + else { + return true; + } + } + }, + + /** + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * @param {Array} [objects] objects array to look into + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @return {fabric.Object} object that contains pointer + * @private + */ + _searchPossibleTargets: function(objects, pointer) { + // Cache all targets where their bounding box contains point. + var target, i = objects.length, subTarget; + // Do not check for currently grouped objects, since we check the parent group itself. + // until we call this function specifically to search inside the activeGroup + while (i--) { + var objToCheck = objects[i]; + var pointerToUse = objToCheck.group ? + this._normalizePointer(objToCheck.group, pointer) : pointer; + if (this._checkTarget(pointerToUse, objToCheck, pointer)) { + target = objects[i]; + if (target.subTargetCheck && target instanceof fabric.Group) { + subTarget = this._searchPossibleTargets(target._objects, pointer); + subTarget && this.targets.push(subTarget); + } + break; + } + } + return target; + }, + + /** + * Returns pointer coordinates without the effect of the viewport + * @param {Object} pointer with "x" and "y" number values + * @return {Object} object with "x" and "y" number values + */ + restorePointerVpt: function(pointer) { + return fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + }, + + /** + * Returns pointer coordinates relative to canvas. + * Can return coordinates with or without viewportTransform. + * ignoreZoom false gives back coordinates that represent + * the point clicked on canvas element. + * ignoreZoom true gives back coordinates after being processed + * by the viewportTransform ( sort of coordinates of what is displayed + * on the canvas where you are clicking. + * ignoreZoom true = HTMLElement coordinates relative to top,left + * ignoreZoom false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates + * compatible with the object.oCoords system. + * of the time. + * @param {Event} e + * @param {Boolean} ignoreZoom + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e, ignoreZoom) { + // return cached values if we are in the event processing chain + if (this._absolutePointer && !ignoreZoom) { + return this._absolutePointer; + } + if (this._pointer && ignoreZoom) { + return this._pointer; + } + + var pointer = getPointer(e), + upperCanvasEl = this.upperCanvasEl, + bounds = upperCanvasEl.getBoundingClientRect(), + boundsWidth = bounds.width || 0, + boundsHeight = bounds.height || 0, + cssScale; + + if (!boundsWidth || !boundsHeight ) { + if ('top' in bounds && 'bottom' in bounds) { + boundsHeight = Math.abs( bounds.top - bounds.bottom ); + } + if ('right' in bounds && 'left' in bounds) { + boundsWidth = Math.abs( bounds.right - bounds.left ); + } + } + + this.calcOffset(); + pointer.x = pointer.x - this._offset.left; + pointer.y = pointer.y - this._offset.top; + if (!ignoreZoom) { + pointer = this.restorePointerVpt(pointer); + } + + var retinaScaling = this.getRetinaScaling(); + if (retinaScaling !== 1) { + pointer.x /= retinaScaling; + pointer.y /= retinaScaling; + } + + if (boundsWidth === 0 || boundsHeight === 0) { + // If bounds are not available (i.e. not visible), do not apply scale. + cssScale = { width: 1, height: 1 }; + } + else { + cssScale = { + width: upperCanvasEl.width / boundsWidth, + height: upperCanvasEl.height / boundsHeight + }; + } + + return { + x: pointer.x * cssScale.width, + y: pointer.y * cssScale.height + }; + }, + + /** + * @private + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), + lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; + + // there is no need to create a new upperCanvas element if we have already one. + if (upperCanvasEl) { + upperCanvasEl.className = ''; + } + else { + upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl = upperCanvasEl; + } + fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); + + this.wrapperEl.appendChild(upperCanvasEl); + + this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); + this._applyCanvasStyle(upperCanvasEl); + this.contextTop = upperCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.width + 'px', + height: this.height + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @param {HTMLElement} element canvas element to apply styles on + */ + _applyCanvasStyle: function (element) { + var width = this.width || element.width, + height = this.height || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0, + 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', + '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Copy the entire inline style from one element (fromEl) to another (toEl) + * @private + * @param {Element} fromEl Element style is copied from + * @param {Element} toEl Element copied style is applied to + */ + _copyCanvasStyle: function (fromEl, toEl) { + toEl.style.cssText = fromEl.style.cssText; + }, + + /** + * Returns context of canvas where object selection is drawn + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Returns currently active object + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Returns an array with the current selected objects + * @return {fabric.Object} active object + */ + getActiveObjects: function () { + var active = this._activeObject; + if (active) { + if (active.type === 'activeSelection' && active._objects) { + return active._objects.slice(0); + } + else { + return [active]; + } + } + return []; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (obj === this._activeObject) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared', { target: obj }); + obj.fire('deselected'); + } + if (obj === this._hoveredTarget){ + this._hoveredTarget = null; + this._hoveredTargets = []; + } + this.callSuper('_onObjectRemoved', obj); + }, + + /** + * @private + * Compares the old activeObject with the current one and fires correct events + * @param {fabric.Object} obj old activeObject + */ + _fireSelectionEvents: function(oldObjects, e) { + var somethingChanged = false, objects = this.getActiveObjects(), + added = [], removed = []; + oldObjects.forEach(function(oldObject) { + if (objects.indexOf(oldObject) === -1) { + somethingChanged = true; + oldObject.fire('deselected', { + e: e, + target: oldObject + }); + removed.push(oldObject); + } + }); + objects.forEach(function(object) { + if (oldObjects.indexOf(object) === -1) { + somethingChanged = true; + object.fire('selected', { + e: e, + target: object + }); + added.push(object); + } + }); + if (oldObjects.length > 0 && objects.length > 0) { + somethingChanged && this.fire('selection:updated', { + e: e, + selected: added, + deselected: removed, + // added for backward compatibility + // deprecated + updated: added[0] || removed[0], + target: this._activeObject, + }); + } + else if (objects.length > 0) { + this.fire('selection:created', { + e: e, + selected: added, + target: this._activeObject, + }); + } + else if (oldObjects.length > 0) { + this.fire('selection:cleared', { + e: e, + deselected: removed, + }); + } + }, + + /** + * Sets given object as the only active object on canvas + * @param {fabric.Object} object Object to set as an active one + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + var currentActives = this.getActiveObjects(); + this._setActiveObject(object, e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * This is a private method for now. + * This is supposed to be equivalent to setActiveObject but without firing + * any event. There is commitment to have this stay this way. + * This is the functional part of setActiveObject. + * @private + * @param {Object} object to set as active + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {Boolean} true if the selection happened + */ + _setActiveObject: function(object, e) { + if (this._activeObject === object) { + return false; + } + if (!this._discardActiveObject(e, object)) { + return false; + } + if (object.onSelect({ e: e })) { + return false; + } + this._activeObject = object; + return true; + }, + + /** + * This is a private method for now. + * This is supposed to be equivalent to discardActiveObject but without firing + * any events. There is commitment to have this stay this way. + * This is the functional part of discardActiveObject. + * @param {Event} [e] Event (passed along when firing "object:deselected") + * @param {Object} object to set as active + * @return {Boolean} true if the selection happened + * @private + */ + _discardActiveObject: function(e, object) { + var obj = this._activeObject; + if (obj) { + // onDeselect return TRUE to cancel selection; + if (obj.onDeselect({ e: e, object: object })) { + return false; + } + this._activeObject = null; + } + return true; + }, + + /** + * Discards currently active object and fire events. If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parameter and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @param {event} e + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); + if (currentActives.length) { + this.fire('before:selection:cleared', { target: activeObject, e: e }); + } + this._discardActiveObject(e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * Clears a canvas element and removes all event listeners + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + var wrapper = this.wrapperEl; + this.removeListeners(); + wrapper.removeChild(this.upperCanvasEl); + wrapper.removeChild(this.lowerCanvasEl); + this.contextCache = null; + this.contextTop = null; + ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + if (wrapper.parentNode) { + wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); + } + delete this.wrapperEl; + fabric.StaticCanvas.prototype.dispose.call(this); + return this; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + // this.discardActiveGroup(); + this.discardActiveObject(); + this.clearContext(this.contextTop); + return this.callSuper('clear'); + }, + + /** + * Draws objects' controls (borders/controls) + * @param {CanvasRenderingContext2D} ctx Context to render controls on + */ + drawControls: function(ctx) { + var activeObject = this._activeObject; + + if (activeObject) { + activeObject._renderControls(ctx); + } + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + //If the object is part of the current selection group, it should + //be transformed appropriately + //i.e. it should be serialised as it would appear if the selection group + //were to be destroyed. + var originalProperties = this._realizeGroupTransformOnObject(instance), + object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); + //Undo the damage we did by changing all of its properties + this._unwindGroupTransformOnObject(instance, originalProperties); + return object; + }, + + /** + * Realises an object's group transformation on it + * @private + * @param {fabric.Object} [instance] the object to transform (gets mutated) + * @returns the original values of instance which were changed + */ + _realizeGroupTransformOnObject: function(instance) { + if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { + var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; + //Copy all the positionally relevant properties across now + var originalValues = {}; + layoutProps.forEach(function(prop) { + originalValues[prop] = instance[prop]; + }); + fabric.util.addTransformToObject(instance, this._activeObject.calcOwnMatrix()); + return originalValues; + } + else { + return null; + } + }, + + /** + * Restores the changed properties of instance + * @private + * @param {fabric.Object} [instance] the object to un-transform (gets mutated) + * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject + */ + _unwindGroupTransformOnObject: function(instance, originalValues) { + if (originalValues) { + instance.set(originalValues); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + //If the object is in a selection group, simulate what would happen to that + //object when the group is deselected + var originalProperties = this._realizeGroupTransformOnObject(instance); + this.callSuper('_setSVGObject', markup, instance, reviver); + this._unwindGroupTransformOnObject(instance, originalProperties); + }, + + setViewportTransform: function (vpt) { + if (this.renderOnAddRemove && this._activeObject && this._activeObject.isEditing) { + this._activeObject.clearContextTop(); + } + fabric.StaticCanvas.prototype.setViewportTransform.call(this, vpt); + } + }); + + // copying static properties manually to work around Opera's bug, + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } +})(); + + +(function() { + + var addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + RIGHT_CLICK = 3, MIDDLE_CLICK = 2, LEFT_CLICK = 1, + addEventOptions = { passive: false }; + + function checkClick(e, value) { + return e.button && (e.button === value - 1); + } + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Contains the id of the touch event that owns the fabric transform + * @type Number + * @private + */ + mainTouchId: null, + + /** + * Adds mouse listeners to canvas + * @private + */ + _initEventListeners: function () { + // in case we initialized the class twice. This should not happen normally + // but in some kind of applications where the canvas element may be changed + // this is a workaround to having double listeners. + this.removeListeners(); + this._bindEvents(); + this.addOrRemove(addListener, 'add'); + }, + + /** + * return an event prefix pointer or mouse. + * @private + */ + _getEventPrefix: function () { + return this.enablePointerEvents ? 'pointer' : 'mouse'; + }, + + addOrRemove: function(functor, eventjsFunctor) { + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + functor(fabric.window, 'resize', this._onResize); + functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + functor(canvasElement, eventTypePrefix + 'out', this._onMouseOut); + functor(canvasElement, eventTypePrefix + 'enter', this._onMouseEnter); + functor(canvasElement, 'wheel', this._onMouseWheel); + functor(canvasElement, 'contextmenu', this._onContextMenu); + functor(canvasElement, 'dblclick', this._onDoubleClick); + functor(canvasElement, 'dragover', this._onDragOver); + functor(canvasElement, 'dragenter', this._onDragEnter); + functor(canvasElement, 'dragleave', this._onDragLeave); + functor(canvasElement, 'drop', this._onDrop); + if (!this.enablePointerEvents) { + functor(canvasElement, 'touchstart', this._onTouchStart, addEventOptions); + } + if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { + eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); + eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); + eventjs[eventjsFunctor](canvasElement, 'orientation', this._onOrientationChange); + eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); + eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress); + } + }, + + /** + * Removes all event listeners + */ + removeListeners: function() { + this.addOrRemove(removeListener, 'remove'); + // if you dispose on a mouseDown, before mouse up, you need to clean document to... + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + }, + + /** + * @private + */ + _bindEvents: function() { + if (this.eventsBound) { + // for any reason we pass here twice we do not want to bind events twice. + return; + } + this._onMouseDown = this._onMouseDown.bind(this); + this._onTouchStart = this._onTouchStart.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onLongPress = this._onLongPress.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseEnter = this._onMouseEnter.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onDoubleClick = this._onDoubleClick.bind(this); + this._onDragOver = this._onDragOver.bind(this); + this._onDragEnter = this._simpleEventHandler.bind(this, 'dragenter'); + this._onDragLeave = this._simpleEventHandler.bind(this, 'dragleave'); + this._onDrop = this._simpleEventHandler.bind(this, 'drop'); + this.eventsBound = true; + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, self) { + this.__onTransformGesture && this.__onTransformGesture(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, self) { + this.__onDrag && this.__onDrag(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on wheel event + */ + _onMouseWheel: function(e) { + this.__onMouseWheel(e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseOut: function(e) { + var target = this._hoveredTarget; + this.fire('mouse:out', { target: target, e: e }); + this._hoveredTarget = null; + target && target.fire('mouseout', { e: e }); + + var _this = this; + this._hoveredTargets.forEach(function(_target){ + _this.fire('mouse:out', { target: target, e: e }); + _target && target.fire('mouseout', { e: e }); + }); + this._hoveredTargets = []; + + if (this._iTextInstances) { + this._iTextInstances.forEach(function(obj) { + if (obj.isEditing) { + obj.hiddenTextarea.focus(); + } + }); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mouseenter + */ + _onMouseEnter: function(e) { + // This find target and consequent 'mouse:over' is used to + // clear old instances on hovered target. + // calling findTarget has the side effect of killing target.__corner. + // as a short term fix we are not firing this if we are currently transforming. + // as a long term fix we need to separate the action of finding a target with the + // side effects we added to it. + if (!this._currentTransform && !this.findTarget(e)) { + this.fire('mouse:over', { target: null, e: e }); + this._hoveredTarget = null; + this._hoveredTargets = []; + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e, self) { + this.__onOrientationChange && this.__onOrientationChange(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e, self) { + this.__onShake && this.__onShake(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onLongPress: function(e, self) { + this.__onLongPress && this.__onLongPress(e, self); + }, + + /** + * prevent default to allow drop event to be fired + * @private + * @param {Event} [e] Event object fired on Event.js shake + */ + _onDragOver: function(e) { + e.preventDefault(); + var target = this._simpleEventHandler('dragover', e); + this._fireEnterLeaveEvents(target, e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onContextMenu: function (e) { + if (this.stopContextMenu) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onDoubleClick: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'dblclick'); + this._resetTransformEventData(e); + }, + + /** + * Return a the id of an event. + * returns either the pointerId or the identifier or 0 for the mouse event + * @private + * @param {Event} evt Event object + */ + getPointerId: function(evt) { + var changedTouches = evt.changedTouches; + + if (changedTouches) { + return changedTouches[0] && changedTouches[0].identifier; + } + + if (this.enablePointerEvents) { + return evt.pointerId; + } + + return -1; + }, + + /** + * Determines if an event has the id of the event that is considered main + * @private + * @param {evt} event Event object + */ + _isMainEvent: function(evt) { + if (evt.isPrimary === true) { + return true; + } + if (evt.isPrimary === false) { + return false; + } + if (evt.type === 'touchend' && evt.touches.length === 0) { + return true; + } + if (evt.changedTouches) { + return evt.changedTouches[0].identifier === this.mainTouchId; + } + return true; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchStart: function(e) { + e.preventDefault(); + if (this.mainTouchId === null) { + this.mainTouchId = this.getPointerId(e); + } + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + addListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + addListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + // Unbind mousedown to prevent double triggers from touch devices + removeListener(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown: function (e) { + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchEnd: function(e) { + if (e.touches.length > 0) { + // if there are still touches stop here + return; + } + this.__onMouseUp(e); + this._resetTransformEventData(); + this.mainTouchId = null; + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + var _this = this; + if (this._willAddMouseDown) { + clearTimeout(this._willAddMouseDown); + } + this._willAddMouseDown = setTimeout(function() { + // Wait 400ms before rebinding mousedown to prevent double triggers + // from touch devices + addListener(_this.upperCanvasEl, eventTypePrefix + 'down', _this._onMouseDown); + _this._willAddMouseDown = 0; + }, 400); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUp: function (e) { + this.__onMouseUp(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + if (this._isMainEvent(e)) { + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMove: function (e) { + !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); + this.__onMouseMove(e); + }, + + /** + * @private + */ + _onResize: function () { + this.calcOffset(); + }, + + /** + * Decides whether the canvas should be redrawn in mouseup and mousedown events. + * @private + * @param {Object} target + */ + _shouldRender: function(target) { + var activeObject = this._activeObject; + + if ( + !!activeObject !== !!target || + (activeObject && target && (activeObject !== target)) + ) { + // this covers: switch of target, from target to no target, selection of target + // multiSelection with key and mouse + return true; + } + else if (activeObject && activeObject.isEditing) { + // if we mouse up/down over a editing textbox a cursor change, + // there is no need to re render + return false; + } + return false; + }, + + /** + * Method that defines the actions when mouse is released on canvas. + * The method resets the currentTransform parameters, store the image corner + * position in the image object and render the canvas on top. + * @private + * @param {Event} e Event object fired on mouseup + */ + __onMouseUp: function (e) { + var target, transform = this._currentTransform, + groupSelector = this._groupSelector, shouldRender = false, + isClick = (!groupSelector || (groupSelector.left === 0 && groupSelector.top === 0)); + this._cacheTransformEventData(e); + target = this._target; + this._handleEvent(e, 'up:before'); + // if right/middle click just fire events and return + // target undefined will make the _handleEvent search the target + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'up', RIGHT_CLICK, isClick); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); + } + this._resetTransformEventData(); + return; + } + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._onMouseUpInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + if (transform) { + this._finalizeCurrentTransform(e); + shouldRender = transform.actionPerformed; + } + if (!isClick) { + var targetWasActive = target === this._activeObject; + this._maybeGroupObjects(e); + if (!shouldRender) { + shouldRender = ( + this._shouldRender(target) || + (!targetWasActive && target === this._activeObject) + ); + } + } + if (target) { + if (target.selectable && target !== this._activeObject && target.activeOn === 'up') { + this.setActiveObject(target, e); + shouldRender = true; + } + else { + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + var control = target.controls[corner], + mouseUpHandler = control && control.getMouseUpHandler(e, target, control); + if (mouseUpHandler) { + var pointer = this.getPointer(e); + mouseUpHandler(e, transform, pointer.x, pointer.y); + } + } + target.isMoving = false; + } + this._setCursorFromEvent(e, target); + this._handleEvent(e, 'up', LEFT_CLICK, isClick); + this._groupSelector = null; + this._currentTransform = null; + // reset the target information about which corner is selected + target && (target.__corner = 0); + if (shouldRender) { + this.requestRenderAll(); + } + else if (!isClick) { + this.renderTop(); + } + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @return {Fabric.Object} target return the the target found, for internal reasons. + */ + _simpleEventHandler: function(eventType, e) { + var target = this.findTarget(e), + targets = this.targets, + options = { + e: e, + target: target, + subTargets: targets, + }; + this.fire(eventType, options); + target && target.fire(eventType, options); + if (!targets) { + return target; + } + for (var i = 0; i < targets.length; i++) { + targets[i].fire(eventType, options); + } + return target; + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @param {fabric.Object} targetObj receiving event + * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right + * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. + */ + _handleEvent: function(e, eventType, button, isClick) { + var target = this._target, + targets = this.targets || [], + options = { + e: e, + target: target, + subTargets: targets, + button: button || LEFT_CLICK, + isClick: isClick || false, + pointer: this._pointer, + absolutePointer: this._absolutePointer, + transform: this._currentTransform + }; + if (eventType === 'up') { + options.currentTarget = this.findTarget(e); + options.currentSubTargets = this.targets; + } + this.fire('mouse:' + eventType, options); + target && target.fire('mouse' + eventType, options); + for (var i = 0; i < targets.length; i++) { + targets[i].fire('mouse' + eventType, options); + } + }, + + /** + * @private + * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event + */ + _finalizeCurrentTransform: function(e) { + + var transform = this._currentTransform, + target = transform.target, + eventName, + options = { + e: e, + target: target, + transform: transform, + action: transform.action, + }; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { + if (transform.actionPerformed) { + // this is not friendly to the new control api. + // is deprecated. + eventName = this._addEventOptions(options, transform); + this._fire(eventName, options); + } + this._fire('modified', options); + } + }, + + /** + * Mutate option object in order to add by property and give back the event name. + * @private + * @deprecated since 4.2.0 + * @param {Object} options to mutate + * @param {Object} transform to inspect action from + */ + _addEventOptions: function(options, transform) { + // we can probably add more details at low cost + // scale change, rotation changes, translation changes + var eventName, by; + switch (transform.action) { + case 'scaleX': + eventName = 'scaled'; + by = 'x'; + break; + case 'scaleY': + eventName = 'scaled'; + by = 'y'; + break; + case 'skewX': + eventName = 'skewed'; + by = 'x'; + break; + case 'skewY': + eventName = 'skewed'; + by = 'y'; + break; + case 'scale': + eventName = 'scaled'; + by = 'equally'; + break; + case 'rotate': + eventName = 'rotated'; + break; + case 'drag': + eventName = 'moved'; + break; + } + options.by = by; + return eventName; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDownInDrawingMode: function(e) { + this._isCurrentlyDrawing = true; + if (this.getActiveObject()) { + this.discardActiveObject(e).requestRenderAll(); + } + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); + this._handleEvent(e, 'down'); + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseMove(pointer, { e: e, pointer: pointer }); + } + this.setCursor(this.freeDrawingCursor); + this._handleEvent(e, 'move'); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + var pointer = this.getPointer(e); + this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); + this._handleEvent(e, 'up'); + }, + + /** + * Method that defines the actions when mouse is clicked on canvas. + * The method inits the currentTransform parameters and renders all the + * canvas so the current image can be placed on the top canvas and the rest + * in on the container one. + * @private + * @param {Event} e Event object fired on mousedown + */ + __onMouseDown: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'down:before'); + var target = this._target; + // if right click just fire events + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'down', RIGHT_CLICK); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'down', MIDDLE_CLICK); + } + return; + } + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) { + return; + } + + var pointer = this._pointer; + // save pointer for check in __onMouseUp event + this._previousPointer = pointer; + var shouldRender = this._shouldRender(target), + shouldGroup = this._shouldGroup(e, target); + if (this._shouldClearSelection(e, target)) { + this.discardActiveObject(e); + } + else if (shouldGroup) { + this._handleGrouping(e, target); + target = this._activeObject; + } + + if (this.selection && (!target || + (!target.selectable && !target.isEditing && target !== this._activeObject))) { + this._groupSelector = { + ex: this._absolutePointer.x, + ey: this._absolutePointer.y, + top: 0, + left: 0 + }; + } + + if (target) { + var alreadySelected = target === this._activeObject; + if (target.selectable && target.activeOn === 'down') { + this.setActiveObject(target, e); + } + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + target.__corner = corner; + if (target === this._activeObject && (corner || !shouldGroup)) { + this._setupCurrentTransform(e, target, alreadySelected); + var control = target.controls[corner], + pointer = this.getPointer(e), + mouseDownHandler = control && control.getMouseDownHandler(e, target, control); + if (mouseDownHandler) { + mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y); + } + } + } + this._handleEvent(e, 'down'); + // we must renderAll so that we update the visuals + (shouldRender || shouldGroup) && this.requestRenderAll(); + }, + + /** + * reset cache form common information needed during event processing + * @private + */ + _resetTransformEventData: function() { + this._target = null; + this._pointer = null; + this._absolutePointer = null; + }, + + /** + * Cache common information needed during event processing + * @private + * @param {Event} e Event object fired on event + */ + _cacheTransformEventData: function(e) { + // reset in order to avoid stale caching + this._resetTransformEventData(); + this._pointer = this.getPointer(e, true); + this._absolutePointer = this.restorePointerVpt(this._pointer); + this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null; + }, + + /** + * @private + */ + _beforeTransform: function(e) { + var t = this._currentTransform; + this.stateful && t.target.saveState(); + this.fire('before:transform', { + e: e, + transform: t, + }); + }, + + /** + * Method that defines the actions when mouse is hovering the canvas. + * The currentTransform parameter will define whether the user is rotating/scaling/translating + * an image or neither of them (only hovering). A group selection is also possible and would cancel + * all any other type of action. + * In case of an image transformation only the top canvas will be rendered. + * @private + * @param {Event} e Event object fired on mousemove + */ + __onMouseMove: function (e) { + this._handleEvent(e, 'move:before'); + this._cacheTransformEventData(e); + var target, pointer; + + if (this.isDrawingMode) { + this._onMouseMoveInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + pointer = this._absolutePointer; + + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; + + this.renderTop(); + } + else if (!this._currentTransform) { + target = this.findTarget(e) || null; + this._setCursorFromEvent(e, target); + this._fireOverOutEvents(target, e); + } + else { + this._transformObject(e); + } + this._handleEvent(e, 'move'); + this._resetTransformEventData(); + }, + + /** + * Manage the mouseout, mouseover events for the fabric object on the canvas + * @param {Fabric.Object} target the target where the target from the mousemove event + * @param {Event} e Event object fired on mousemove + * @private + */ + _fireOverOutEvents: function(target, e) { + var _hoveredTarget = this._hoveredTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _hoveredTarget, + evtOut: 'mouseout', + canvasEvtOut: 'mouse:out', + evtIn: 'mouseover', + canvasEvtIn: 'mouse:over', + }); + for (var i = 0; i < length; i++){ + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'mouseout', + evtIn: 'mouseover', + }); + } + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + }, + + /** + * Manage the dragEnter, dragLeave events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the onDrag event + * @param {Event} e Event object fired on ondrag + * @private + */ + _fireEnterLeaveEvents: function(target, e) { + var _draggedoverTarget = this._draggedoverTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _draggedoverTarget, + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + for (var i = 0; i < length; i++) { + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + } + this._draggedoverTarget = target; + }, + + /** + * Manage the synthetic in/out events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the supported events + * @param {Event} e Event object fired + * @param {Object} config configuration for the function to work + * @param {String} config.targetName property on the canvas where the old target is stored + * @param {String} [config.canvasEvtOut] name of the event to fire at canvas level for out + * @param {String} config.evtOut name of the event to fire for out + * @param {String} [config.canvasEvtIn] name of the event to fire at canvas level for in + * @param {String} config.evtIn name of the event to fire for in + * @private + */ + fireSyntheticInOutEvents: function(target, e, config) { + var inOpt, outOpt, oldTarget = config.oldTarget, outFires, inFires, + targetChanged = oldTarget !== target, canvasEvtIn = config.canvasEvtIn, canvasEvtOut = config.canvasEvtOut; + if (targetChanged) { + inOpt = { e: e, target: target, previousTarget: oldTarget }; + outOpt = { e: e, target: oldTarget, nextTarget: target }; + } + inFires = target && targetChanged; + outFires = oldTarget && targetChanged; + if (outFires) { + canvasEvtOut && this.fire(canvasEvtOut, outOpt); + oldTarget.fire(config.evtOut, outOpt); + } + if (inFires) { + canvasEvtIn && this.fire(canvasEvtIn, inOpt); + target.fire(config.evtIn, inOpt); + } + }, + + /** + * Method that defines actions when an Event Mouse Wheel + * @param {Event} e Event object fired on mouseup + */ + __onMouseWheel: function(e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'wheel'); + this._resetTransformEventData(); + }, + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + var pointer = this.getPointer(e), + transform = this._currentTransform; + + transform.reset = false; + transform.shiftKey = e.shiftKey; + transform.altKey = e[this.centeredKey]; + + this._performTransformAction(e, transform, pointer); + transform.actionPerformed && this.requestRenderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + action = transform.action, + actionPerformed = false, + actionHandler = transform.actionHandler; + // this object could be created from the function in the control handlers + + + if (actionHandler) { + actionPerformed = actionHandler(e, transform, x, y); + } + if (action === 'drag' && actionPerformed) { + transform.target.isMoving = true; + this.setCursor(transform.target.moveCursor || this.moveCursor); + } + transform.actionPerformed = transform.actionPerformed || actionPerformed; + }, + + /** + * @private + */ + _fire: fabric.controlsUtils.fireEvent, + + /** + * Sets the cursor depending on where the canvas is being hovered. + * Note: very buggy in Opera + * @param {Event} e Event object + * @param {Object} target Object that the mouse is hovering, if so. + */ + _setCursorFromEvent: function (e, target) { + if (!target) { + this.setCursor(this.defaultCursor); + return false; + } + var hoverCursor = target.hoverCursor || this.hoverCursor, + activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? + this._activeObject : null, + // only show proper corner when group selection is not active + corner = (!activeSelection || !activeSelection.contains(target)) + // here we call findTargetCorner always with undefined for the touch parameter. + // we assume that if you are using a cursor you do not need to interact with + // the bigger touch area. + && target._findTargetCorner(this.getPointer(e, true)); + + if (!corner) { + if (target.subTargetCheck){ + // hoverCursor should come from top-most subTarget, + // so we walk the array backwards + this.targets.concat().reverse().map(function(_target){ + hoverCursor = _target.hoverCursor || hoverCursor; + }); + } + this.setCursor(hoverCursor); + } + else { + this.setCursor(this.getCornerCursor(corner, target, e)); + } + }, + + /** + * @private + */ + getCornerCursor: function(corner, target, e) { + var control = target.controls[corner]; + return control.cursorStyleHandler(e, control, target); + } + }); +})(); + + +(function() { + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this._activeObject; + return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + var activeObject = this._activeObject; + // avoid multi select when shift click on a corner + if (activeObject.__corner) { + return; + } + if (target === activeObject) { + // if it's a group, find target again, using activeGroup objects + target = this.findTarget(e, true); + // if even object is not found or we are on activeObjectCorner, bail out + if (!target || !target.selectable) { + return; + } + } + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(target, e); + } + else { + this._createActiveSelection(target, e); + } + }, + + /** + * @private + */ + _updateActiveSelection: function(target, e) { + var activeSelection = this._activeObject, + currentActiveObjects = activeSelection._objects.slice(0); + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + if (activeSelection.size() === 1) { + // activate last remaining object + this._setActiveObject(activeSelection.item(0), e); + } + } + else { + activeSelection.addWithUpdate(target); + this._hoveredTarget = activeSelection; + this._hoveredTargets = this.targets.concat(); + } + this._fireSelectionEvents(currentActiveObjects, e); + }, + + /** + * @private + */ + _createActiveSelection: function(target, e) { + var currentActives = this.getActiveObjects(), group = this._createGroup(target); + this._hoveredTarget = group; + // ISSUE 4115: should we consider subTargets here? + // this._hoveredTargets = []; + // this._hoveredTargets = this.targets.concat(); + this._setActiveObject(group, e); + this._fireSelectionEvents(currentActives, e); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + var objects = this._objects, + isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), + groupObjects = isActiveLower + ? [this._activeObject, target] + : [target, this._activeObject]; + this._activeObject.isEditing && this._activeObject.exitEditing(); + return new fabric.ActiveSelection(groupObjects, { + canvas: this + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(e), + aGroup; + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + aGroup = new fabric.ActiveSelection(group.reverse(), { + canvas: this + }); + this.setActiveObject(aGroup, e); + } + }, + + /** + * @private + */ + _collectObjects: function(e) { + var group = [], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + allowIntersect = !this.selectionFullyContained, + isClick = x1 === x2 && y1 === y2; + // we iterate reverse order to collect top first in case of click. + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) { + continue; + } + + if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2, true)) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2, true) || + (allowIntersect && currentObject.containsPoint(selectionX1Y1, null, true)) || + (allowIntersect && currentObject.containsPoint(selectionX2Y2, null, true)) + ) { + group.push(currentObject); + // only add one object if it's a click + if (isClick) { + break; + } + } + } + + if (group.length > 1) { + group = group.filter(function(object) { + return !object.onSelect({ e: e }); + }); + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + this.setCursor(this.defaultCursor); + // clear selection and current transformation + this._groupSelector = null; + } + }); + +})(); + + +(function () { + fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately + * @param {Object} [options] Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @example Generate jpeg dataURL with lower quality + * var dataURL = canvas.toDataURL({ + * format: 'jpeg', + * quality: 0.8 + * }); + * @example Generate cropped png dataURL (clipping of canvas) + * var dataURL = canvas.toDataURL({ + * format: 'png', + * left: 100, + * top: 100, + * width: 200, + * height: 200 + * }); + * @example Generate double scaled png dataURL + * var dataURL = canvas.toDataURL({ + * format: 'png', + * multiplier: 2 + * }); + */ + toDataURL: function (options) { + options || (options = { }); + + var format = options.format || 'png', + quality = options.quality || 1, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), + canvasEl = this.toCanvasElement(multiplier, options); + return fabric.util.toDataURL(canvasEl, format, quality); + }, + + /** + * Create a new HTMLCanvas element painted with the current canvas content. + * No need to resize the actual one or repaint it. + * Will transfer object ownership to a new canvas, paint it, and set everything back. + * This is an intermediary step used to get to a dataUrl but also it is useful to + * create quick image copies of a canvas without passing for the dataUrl string + * @param {Number} [multiplier] a zoom factor. + * @param {Object} [cropping] Cropping informations + * @param {Number} [cropping.left] Cropping left offset. + * @param {Number} [cropping.top] Cropping top offset. + * @param {Number} [cropping.width] Cropping width. + * @param {Number} [cropping.height] Cropping height. + */ + toCanvasElement: function(multiplier, cropping) { + multiplier = multiplier || 1; + cropping = cropping || { }; + var scaledWidth = (cropping.width || this.width) * multiplier, + scaledHeight = (cropping.height || this.height) * multiplier, + zoom = this.getZoom(), + originalWidth = this.width, + originalHeight = this.height, + newZoom = zoom * multiplier, + vp = this.viewportTransform, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, + originalInteractive = this.interactive, + newVp = [newZoom, 0, 0, newZoom, translateX, translateY], + originalRetina = this.enableRetinaScaling, + canvasEl = fabric.util.createCanvasElement(), + originalContextTop = this.contextTop; + canvasEl.width = scaledWidth; + canvasEl.height = scaledHeight; + this.contextTop = null; + this.enableRetinaScaling = false; + this.interactive = false; + this.viewportTransform = newVp; + this.width = scaledWidth; + this.height = scaledHeight; + this.calcViewportBoundaries(); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); + this.viewportTransform = vp; + this.width = originalWidth; + this.height = originalHeight; + this.calcViewportBoundaries(); + this.interactive = originalInteractive; + this.enableRetinaScaling = originalRetina; + this.contextTop = originalContextTop; + return canvasEl; + }, + }); + +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + /** + * Populates canvas with data from the specified JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} + * @example loadFromJSON + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * @example loadFromJSON with reviver + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * // `o` = json object + * // `object` = fabric.Object instance + * // ... do some stuff ... + * }); + */ + loadFromJSON: function (json, callback, reviver) { + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : fabric.util.object.clone(json); + + var _this = this, + clipPath = serialized.clipPath, + renderOnAddRemove = this.renderOnAddRemove; + + this.renderOnAddRemove = false; + + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); + _this._setBgOverlay(serialized, function () { + if (clipPath) { + _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { + _this.clipPath = enlivenedCanvasClip[0]; + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + }); + } + else { + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + } + }); + }, reviver); + return this; + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Array} restored canvas objects + * @param {Function} cached renderOnAddRemove callback + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { + var _this = this; + enlivenedObjects.forEach(function(obj, index) { + // we splice the array just in case some custom classes restored from JSON + // will add more object to canvas at canvas init. + _this.insertAt(obj, index); + }); + this.renderOnAddRemove = renderOnAddRemove; + // remove parts i cannot set as options + delete serialized.objects; + delete serialized.backgroundImage; + delete serialized.overlayImage; + delete serialized.background; + delete serialized.overlay; + // this._initOptions does too many things to just + // call it. Normally loading an Object from JSON + // create the Object instance. Here the Canvas is + // already an instance and we are just loading things over it + this._setOptions(serialized); + this.renderAll(); + callback && callback(); + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + }, + + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + if (!objects || objects.length === 0) { + callback && callback([]); + return; + } + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + */ + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.util.createCanvasElement(); + + el.width = this.width; + el.height = this.height; + + var clone = new fabric.Canvas(el); + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + degreesToRadians = fabric.util.degreesToRadians, + objectCaching = !fabric.isLikelyNode, + ALIASING_LIMIT = 2; + + if (fabric.Object) { + return; + } + + /** + * Root object class from which all 2d shape classes inherit from + * @class fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects} + * @see {@link fabric.Object#initialize} for constructor definition + * + * @fires added + * @fires removed + * + * @fires selected + * @fires deselected + * @fires modified + * @fires modified + * @fires moved + * @fires scaled + * @fires rotated + * @fires skewed + * + * @fires rotating + * @fires scaling + * @fires moving + * @fires skewing + * + * @fires mousedown + * @fires mouseup + * @fires mouseover + * @fires mouseout + * @fires mousewheel + * @fires mousedblclick + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + */ + fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc.). + * Note that this property is meant to be read-only and not meant to be modified. + * If you modify, certain parts of Fabric (such as JSON loading) won't work correctly. + * @type String + * @default + */ + type: 'object', + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originX: 'left', + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originY: 'top', + + /** + * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} + * @type Number + * @default + */ + top: 0, + + /** + * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} + * @type Number + * @default + */ + left: 0, + + /** + * Object width + * @type Number + * @default + */ + width: 0, + + /** + * Object height + * @type Number + * @default + */ + height: 0, + + /** + * Object scale factor (horizontal) + * @type Number + * @default + */ + scaleX: 1, + + /** + * Object scale factor (vertical) + * @type Number + * @default + */ + scaleY: 1, + + /** + * When true, an object is rendered as flipped horizontally + * @type Boolean + * @default + */ + flipX: false, + + /** + * When true, an object is rendered as flipped vertically + * @type Boolean + * @default + */ + flipY: false, + + /** + * Opacity of an object + * @type Number + * @default + */ + opacity: 1, + + /** + * Angle of rotation of an object (in degrees) + * @type Number + * @default + */ + angle: 0, + + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default + */ + skewX: 0, + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default + */ + skewY: 0, + + /** + * Size of object's controlling corners (in pixels) + * @type Number + * @default + */ + cornerSize: 13, + + /** + * Size of object's controlling corners when touch interaction is detected + * @type Number + * @default + */ + touchCornerSize: 24, + + /** + * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) + * @type Boolean + * @default + */ + transparentCorners: true, + + /** + * Default cursor value used when hovering over this object on canvas + * @type String + * @default + */ + hoverCursor: null, + + /** + * Default cursor value used when moving this object on canvas + * @type String + * @default + */ + moveCursor: null, + + /** + * Padding between object and its controlling borders (in pixels) + * @type Number + * @default + */ + padding: 0, + + /** + * Color of controlling borders of an object (when it's active) + * @type String + * @default + */ + borderColor: 'rgb(178,204,255)', + + /** + * Array specifying dash pattern of an object's borders (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + borderDashArray: null, + + /** + * Color of controlling corners of an object (when it's active) + * @type String + * @default + */ + cornerColor: 'rgb(178,204,255)', + + /** + * Color of controlling corners of an object (when it's active and transparentCorners false) + * @since 1.6.2 + * @type String + * @default + */ + cornerStrokeColor: null, + + /** + * Specify style of control, 'rect' or 'circle' + * @since 1.6.2 + * @type String + */ + cornerStyle: 'rect', + + /** + * Array specifying dash pattern of an object's control (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + cornerDashArray: null, + + /** + * When true, this object will use center point as the origin of transformation + * when being scaled via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, this object will use center point as the origin of transformation + * when being rotated via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: true, + + /** + * Color of object's fill + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + fill: 'rgb(0,0,0)', + + /** + * Fill rule used to fill an object + * accepted values are nonzero, evenodd + * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) + * @type String + * @default + */ + fillRule: 'nonzero', + + /** + * Composite rule used for canvas globalCompositeOperation + * @type String + * @default + */ + globalCompositeOperation: 'source-over', + + /** + * Background color of an object. + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + backgroundColor: '', + + /** + * Selection Background color of an object. colored layer behind the object when it is active. + * does not mix good with globalCompositeOperation methods. + * @type String + * @default + */ + selectionBackgroundColor: '', + + /** + * When defined, an object is rendered via stroke and this property specifies its color + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + stroke: null, + + /** + * Width of a stroke used to render this object + * @type Number + * @default + */ + strokeWidth: 1, + + /** + * Array specifying dash pattern of an object's stroke (stroke must be defined) + * @type Array + */ + strokeDashArray: null, + + /** + * Line offset of an object's stroke + * @type Number + * @default + */ + strokeDashOffset: 0, + + /** + * Line endings style of an object's stroke (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'butt', + + /** + * Corner style of an object's stroke (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'miter', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke + * @type Number + * @default + */ + strokeMiterLimit: 4, + + /** + * Shadow object representing shadow of this shape + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Opacity of object's controlling borders when object is active and moving + * @type Number + * @default + */ + borderOpacityWhenMoving: 0.4, + + /** + * Scale factor of object's controlling borders + * bigger number will make a thicker border + * border is 1, so this is basically a border thickness + * since there is no way to change the border itself. + * @type Number + * @default + */ + borderScaleFactor: 1, + + /** + * Minimum allowed scale value of an object + * @type Number + * @default + */ + minScaleLimit: 0, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). + * But events still fire on it. + * @type Boolean + * @default + */ + selectable: true, + + /** + * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 + * @type Boolean + * @default + */ + evented: true, + + /** + * When set to `false`, an object is not rendered on canvas + * @type Boolean + * @default + */ + visible: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @type Boolean + * @default + */ + hasControls: true, + + /** + * When set to `false`, object's controlling borders are not rendered + * @type Boolean + * @default + */ + hasBorders: true, + + /** + * When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * When `false`, default object's values are not included in its serialization + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `true`, object horizontal movement is locked + * @type Boolean + * @default + */ + lockMovementX: false, + + /** + * When `true`, object vertical movement is locked + * @type Boolean + * @default + */ + lockMovementY: false, + + /** + * When `true`, object rotation is locked + * @type Boolean + * @default + */ + lockRotation: false, + + /** + * When `true`, object horizontal scaling is locked + * @type Boolean + * @default + */ + lockScalingX: false, + + /** + * When `true`, object vertical scaling is locked + * @type Boolean + * @default + */ + lockScalingY: false, + + /** + * When `true`, object horizontal skewing is locked + * @type Boolean + * @default + */ + lockSkewingX: false, + + /** + * When `true`, object vertical skewing is locked + * @type Boolean + * @default + */ + lockSkewingY: false, + + /** + * When `true`, object cannot be flipped by scaling into negative values + * @type Boolean + * @default + */ + lockScalingFlip: false, + + /** + * When `true`, object is not exported in OBJECT/JSON + * @since 1.6.3 + * @type Boolean + * @default + */ + excludeFromExport: false, + + /** + * When `true`, object is cached on an additional canvas. + * When `false`, object is not cached unless necessary ( clipPath ) + * default to true + * @since 1.7.0 + * @type Boolean + * @default true + */ + objectCaching: objectCaching, + + /** + * When `true`, object properties are checked for cache invalidation. In some particular + * situation you may want this to be disabled ( spray brush, very big, groups) + * or if your application does not allow you to modify properties for groups child you want + * to disable it for groups. + * default to false + * since 1.7.0 + * @type Boolean + * @default false + */ + statefullCache: false, + + /** + * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled + * too much and will be redrawn with correct details at the end of scaling. + * this setting is performance and application dependant. + * default to true + * since 1.7.0 + * @type Boolean + * @default true + */ + noScaleCache: true, + + /** + * When `false`, the stoke width will scale with the object. + * When `true`, the stroke will always match the exact pixel size entered for stroke width. + * default to false + * @since 2.6.0 + * @type Boolean + * @default false + * @type Boolean + * @default false + */ + strokeUniform: false, + + /** + * When set to `true`, object's cache will be rerendered next render call. + * since 1.7.0 + * @type Boolean + * @default true + */ + dirty: true, + + /** + * keeps the value of the last hovered corner during mouse move. + * 0 is no corner, or 'mt', 'ml', 'mtr' etc.. + * It should be private, but there is no harm in using it as + * a read-only property. + * @type number|string|any + * @default 0 + */ + __corner: 0, + + /** + * Determines if the fill or the stroke is drawn first (one of "fill" or "stroke") + * @type String + * @default + */ + paintFirst: 'fill', + + /** + * When 'down', object is set to active on mousedown/touchstart + * When 'up', object is set to active on mouseup/touchend + * Experimental. Let's see if this breaks anything before supporting officially + * @private + * since 4.4.0 + * @type String + * @default 'down' + */ + activeOn: 'down', + + /** + * List of properties to consider when checking if state + * of an object is changed (fabric.Object#hasStateChanged) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + + 'stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit ' + + 'angle opacity fill globalCompositeOperation shadow visible backgroundColor ' + + 'skewX skewY fillRule paintFirst clipPath strokeUniform' + ).split(' '), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: ( + 'fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform' + + ' strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath' + ).split(' '), + + /** + * List of properties to consider for animating colors. + * @type Array + */ + colorProperties: ( + 'fill stroke backgroundColor' + ).split(' '), + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the object has rendered, and the context is placed in the center + * of the object cacheCanvas. + * If you want 0,0 of a clipPath to align with an object center, use clipPath.originX/Y to 'center' + * @type fabric.Object + */ + clipPath: undefined, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will make the object clip to the outside of the clipPath + * since 2.4.0 + * @type boolean + * @default false + */ + inverted: false, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will have its top and left relative to canvas, and will + * not be influenced by the object transform. This will make the clipPath relative + * to the canvas, but clipping just a particular object. + * WARNING this is beta, this feature may change or be renamed. + * since 2.4.0 + * @type boolean + * @default false + */ + absolutePositioned: false, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Create a the canvas used to keep the cached copy of the object + * @private + */ + _createCacheCanvas: function() { + this._cacheProperties = {}; + this._cacheCanvas = fabric.util.createCanvasElement(); + this._cacheContext = this._cacheCanvas.getContext('2d'); + this._updateCacheCanvas(); + // if canvas gets created, is empty, so dirty. + this.dirty = true; + }, + + /** + * Limit the cache dimensions so that X * Y do not cross fabric.perfLimitSizeTotal + * and each side do not cross fabric.cacheSideLimit + * those numbers are configurable so that you can get as much detail as you want + * making bargain with performances. + * @param {Object} dims + * @param {Object} dims.width width of canvas + * @param {Object} dims.height height of canvas + * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _limitCacheSize: function(dims) { + var perfLimitSizeTotal = fabric.perfLimitSizeTotal, + width = dims.width, height = dims.height, + max = fabric.maxCacheSideLimit, min = fabric.minCacheSideLimit; + if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { + if (width < min) { + dims.width = min; + } + if (height < min) { + dims.height = min; + } + return dims; + } + var ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal), + capValue = fabric.util.capValue, + x = capValue(min, limitedDims.x, max), + y = capValue(min, limitedDims.y, max); + if (width > x) { + dims.zoomX /= width / x; + dims.width = x; + dims.capped = true; + } + if (height > y) { + dims.zoomY /= height / y; + dims.height = y; + dims.capped = true; + } + return dims; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @return {Object}.x width of object to be cached + * @return {Object}.y height of object to be cached + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var objectScale = this.getTotalObjectScaling(), + // caculate dimensions without skewing + dim = this._getTransformedDimensions(0, 0), + neededX = dim.x * objectScale.scaleX / this.scaleX, + neededY = dim.y * objectScale.scaleY / this.scaleY; + return { + // for sure this ALIASING_LIMIT is slightly creating problem + // in situation in which the cache canvas gets an upper limit + // also objectScale contains already scaleX and scaleY + width: neededX + ALIASING_LIMIT, + height: neededY + ALIASING_LIMIT, + zoomX: objectScale.scaleX, + zoomY: objectScale.scaleY, + x: neededX, + y: neededY + }; + }, + + /** + * Update width and height of the canvas for cache + * returns true or false if canvas needed resize. + * @private + * @return {Boolean} true if the canvas has been resized + */ + _updateCacheCanvas: function() { + var targetCanvas = this.canvas; + if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) { + var target = targetCanvas._currentTransform.target, + action = targetCanvas._currentTransform.action; + if (this === target && action.slice && action.slice(0, 5) === 'scale') { + return false; + } + } + var canvas = this._cacheCanvas, + dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + minCacheSize = fabric.minCacheSideLimit, + width = dims.width, height = dims.height, drawingWidth, drawingHeight, + zoomX = dims.zoomX, zoomY = dims.zoomY, + dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight, + zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY, + shouldRedraw = dimensionsChanged || zoomChanged, + additionalWidth = 0, additionalHeight = 0, shouldResizeCanvas = false; + if (dimensionsChanged) { + var canvasWidth = this._cacheCanvas.width, + canvasHeight = this._cacheCanvas.height, + sizeGrowing = width > canvasWidth || height > canvasHeight, + sizeShrinking = (width < canvasWidth * 0.9 || height < canvasHeight * 0.9) && + canvasWidth > minCacheSize && canvasHeight > minCacheSize; + shouldResizeCanvas = sizeGrowing || sizeShrinking; + if (sizeGrowing && !dims.capped && (width > minCacheSize || height > minCacheSize)) { + additionalWidth = width * 0.1; + additionalHeight = height * 0.1; + } + } + if (this instanceof fabric.Text && this.path) { + shouldRedraw = true; + shouldResizeCanvas = true; + additionalWidth += this.getHeightOfLine(0) * this.zoomX; + additionalHeight += this.getHeightOfLine(0) * this.zoomY; + } + if (shouldRedraw) { + if (shouldResizeCanvas) { + canvas.width = Math.ceil(width + additionalWidth); + canvas.height = Math.ceil(height + additionalHeight); + } + else { + this._cacheContext.setTransform(1, 0, 0, 1, 0, 0); + this._cacheContext.clearRect(0, 0, canvas.width, canvas.height); + } + drawingWidth = dims.x / 2; + drawingHeight = dims.y / 2; + this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; + this.cacheTranslationY = Math.round(canvas.height / 2 - drawingHeight) + drawingHeight; + this.cacheWidth = width; + this.cacheHeight = height; + this._cacheContext.translate(this.cacheTranslationX, this.cacheTranslationY); + this._cacheContext.scale(zoomX, zoomY); + this.zoomX = zoomX; + this.zoomY = zoomY; + return true; + } + return false; + }, + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + this._setOptions(options); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); + }, + + /** + * Transforms context when rendering an object + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function(ctx) { + var needFullTransform = (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); + var m = this.calcTransformMatrix(!needFullTransform); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + }, + + /** + * Returns an object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + + object = { + type: this.type, + version: fabric.version, + originX: this.originX, + originY: this.originY, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, + strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), + strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray, + strokeLineCap: this.strokeLineCap, + strokeDashOffset: this.strokeDashOffset, + strokeLineJoin: this.strokeLineJoin, + strokeUniform: this.strokeUniform, + strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.angle, NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, + visible: this.visible, + backgroundColor: this.backgroundColor, + fillRule: this.fillRule, + paintFirst: this.paintFirst, + globalCompositeOperation: this.globalCompositeOperation, + skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS), + }; + + if (this.clipPath && !this.clipPath.excludeFromExport) { + object.clipPath = this.clipPath.toObject(propertiesToInclude); + object.clipPath.inverted = this.clipPath.inverted; + object.clipPath.absolutePositioned = this.clipPath.absolutePositioned; + } + + fabric.util.populateWithProperties(this, object, propertiesToInclude); + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + // will be overwritten by subclasses + return this.toObject(propertiesToInclude); + }, + + /** + * @private + * @param {Object} object + */ + _removeDefaultValues: function(object) { + var prototype = fabric.util.getKlass(object.type).prototype, + stateProperties = prototype.stateProperties; + stateProperties.forEach(function(prop) { + if (prop === 'left' || prop === 'top') { + return; + } + if (object[prop] === prototype[prop]) { + delete object[prop]; + } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + + // basically a check for [] === [] + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + delete object[prop]; + } + }); + + return object; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Return the object scale factor counting also the group scaling + * @return {Object} object with scaleX and scaleY properties + */ + getObjectScaling: function() { + // if the object is a top level one, on the canvas, we go for simple aritmetic + // otherwise the complex method with angles will return approximations and decimals + // and will likely kill the cache when not needed + // https://github.com/fabricjs/fabric.js/issues/7157 + if (!this.group) { + return { + scaleX: this.scaleX, + scaleY: this.scaleY, + }; + } + // if we are inside a group total zoom calculation is complex, we defer to generic matrices + var options = fabric.util.qrDecompose(this.calcTransformMatrix()); + return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; + }, + + /** + * Return the object scale factor counting also the group scaling, zoom and retina + * @return {Object} object with scaleX and scaleY properties + */ + getTotalObjectScaling: function() { + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + if (this.canvas) { + var zoom = this.canvas.getZoom(); + var retina = this.canvas.getRetinaScaling(); + scaleX *= zoom * retina; + scaleY *= zoom * retina; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + + /** + * Return the object opacity counting also the group property + * @return {Number} + */ + getObjectOpacity: function() { + var opacity = this.opacity; + if (this.group) { + opacity *= this.group.getObjectOpacity(); + } + return opacity; + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Object} thisArg + */ + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'), + isChanged = this[key] !== value, groupNeedsUpdate = false; + + if (shouldConstrainValue) { + value = this._constrainScale(value); + } + if (key === 'scaleX' && value < 0) { + this.flipX = !this.flipX; + value *= -1; + } + else if (key === 'scaleY' && value < 0) { + this.flipY = !this.flipY; + value *= -1; + } + else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { + value = new fabric.Shadow(value); + } + else if (key === 'dirty' && this.group) { + this.group.set('dirty', value); + } + + this[key] = value; + + if (isChanged) { + groupNeedsUpdate = this.group && this.group.isOnACache(); + if (this.cacheProperties.indexOf(key) > -1) { + this.dirty = true; + groupNeedsUpdate && this.group.set('dirty', true); + } + else if (groupNeedsUpdate && this.stateProperties.indexOf(key) > -1) { + this.group.set('dirty', true); + } + } + return this; + }, + + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf fabric.Object.prototype + * @return {Array} + */ + getViewportTransform: function() { + if (this.canvas && this.canvas.viewportTransform) { + return this.canvas.viewportTransform; + } + return fabric.iMatrix.concat(); + }, + + /* + * @private + * return if the object would be visible in rendering + * @memberOf fabric.Object.prototype + * @return {Boolean} + */ + isNotVisible: function() { + return this.opacity === 0 || + (!this.width && !this.height && this.strokeWidth === 0) || + !this.visible; + }, + + /** + * Renders an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if width/height are zeros or object is not visible + if (this.isNotVisible()) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + ctx.save(); + this._setupCompositeOperation(ctx); + this.drawSelectionBackground(ctx); + this.transform(ctx); + this._setOpacity(ctx); + this._setShadow(ctx, this); + if (this.shouldCache()) { + this.renderCache(); + this.drawCacheOnCanvas(ctx); + } + else { + this._removeCacheCanvas(); + this.dirty = false; + this.drawObject(ctx); + if (this.objectCaching && this.statefullCache) { + this.saveState({ propertySet: 'cacheProperties' }); + } + } + ctx.restore(); + }, + + renderCache: function(options) { + options = options || {}; + if (!this._cacheCanvas) { + this._createCacheCanvas(); + } + if (this.isCacheDirty()) { + this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, options.forClipping); + this.dirty = false; + } + }, + + /** + * Remove cacheCanvas and its dimensions from the objects + */ + _removeCacheCanvas: function() { + this._cacheCanvas = null; + this.cacheWidth = 0; + this.cacheHeight = 0; + }, + + /** + * return true if the object will draw a stroke + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an approximation and be fast. + * wrote to avoid extra caching, it has to return true when stroke happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the stroke is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasStroke: function() { + return this.stroke && this.stroke !== 'transparent' && this.strokeWidth !== 0; + }, + + /** + * return true if the object will draw a fill + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an approximation and be fast. + * wrote to avoid extra caching, it has to return true when fill happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the fill is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasFill: function() { + return this.fill && this.fill !== 'transparent'; + }, + + /** + * When set to `true`, force the object to have its own cache, even if it is inside a group + * it may be needed when your object behave in a particular way on the cache and always needs + * its own isolated canvas to render correctly. + * Created to be overridden + * since 1.7.12 + * @returns Boolean + */ + needsItsOwnCache: function() { + if (this.paintFirst === 'stroke' && + this.hasFill() && this.hasStroke() && typeof this.shadow === 'object') { + return true; + } + if (this.clipPath) { + return true; + } + return false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * Read as: cache if is needed, or if the feature is enabled but we are not already caching. + * @return {Boolean} + */ + shouldCache: function() { + this.ownCaching = this.needsItsOwnCache() || ( + this.objectCaching && + (!this.group || !this.group.isOnACache()) + ); + return this.ownCaching; + }, + + /** + * Check if this object or a child object will cast a shadow + * used by Group.shouldCache to know if child has a shadow recursively + * @return {Boolean} + */ + willDrawShadow: function() { + return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); + }, + + /** + * Execute the drawing operation for an object clipPath + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCache: function(ctx) { + var path = this.clipPath; + ctx.save(); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4 + if (path.inverted) { + ctx.globalCompositeOperation = 'destination-out'; + } + else { + ctx.globalCompositeOperation = 'destination-in'; + } + //ctx.scale(1 / 2, 1 / 2); + if (path.absolutePositioned) { + var m = fabric.util.invertTransform(this.calcTransformMatrix()); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx, forClipping) { + var originalFill = this.fill, originalStroke = this.stroke; + if (forClipping) { + this.fill = 'black'; + this.stroke = ''; + this._setClippingProperties(ctx); + } + else { + this._renderBackground(ctx); + } + this._render(ctx); + this._drawClipPath(ctx); + this.fill = originalFill; + this.stroke = originalStroke; + }, + + _drawClipPath: function(ctx) { + var path = this.clipPath; + if (!path) { return; } + // needed to setup a couple of variables + // path canvas gets overridden with this one. + // TODO find a better solution? + path.canvas = this.canvas; + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCache(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + ctx.scale(1 / this.zoomX, 1 / this.zoomY); + ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); + }, + + /** + * Check if cache is dirty + * @param {Boolean} skipCanvas skip canvas checks because this object is painted + * on parent canvas. + */ + isCacheDirty: function(skipCanvas) { + if (this.isNotVisible()) { + return false; + } + if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { + // in this case the context is already cleared. + return true; + } + else { + if (this.dirty || + (this.clipPath && this.clipPath.absolutePositioned) || + (this.statefullCache && this.hasStateChanged('cacheProperties')) + ) { + if (this._cacheCanvas && !skipCanvas) { + var width = this.cacheWidth / this.zoomX; + var height = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-width / 2, -height / 2, width, height); + } + return true; + } + } + return false; + }, + + /** + * Draws a background for the object big as its untransformed dimensions + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (!this.backgroundColor) { + return; + } + var dim = this._getNonTransformedDimensions(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + -dim.x / 2, + -dim.y / 2, + dim.x, + dim.y + ); + // if there is background color no other shadows + // should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setOpacity: function(ctx) { + if (this.group && !this.group._transformDone) { + ctx.globalAlpha = this.getObjectOpacity(); + } + else { + ctx.globalAlpha *= this.opacity; + } + }, + + _setStrokeStyles: function(ctx, decl) { + var stroke = decl.stroke; + if (stroke) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = decl.strokeLineCap; + ctx.lineDashOffset = decl.strokeDashOffset; + ctx.lineJoin = decl.strokeLineJoin; + ctx.miterLimit = decl.strokeMiterLimit; + if (stroke.toLive) { + if (stroke.gradientUnits === 'percentage' || stroke.gradientTransform || stroke.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + this._applyPatternForTransformedGradient(ctx, stroke); + } + else { + // is a simple gradient or pattern + ctx.strokeStyle = stroke.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, stroke); + } + } + else { + // is a color + ctx.strokeStyle = decl.stroke; + } + } + }, + + _setFillStyles: function(ctx, decl) { + var fill = decl.fill; + if (fill) { + if (fill.toLive) { + ctx.fillStyle = fill.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, decl.fill); + } + else { + ctx.fillStyle = fill; + } + } + }, + + _setClippingProperties: function(ctx) { + ctx.globalAlpha = 1; + ctx.strokeStyle = 'transparent'; + ctx.fillStyle = '#000000'; + }, + + /** + * @private + * Sets line dash + * @param {CanvasRenderingContext2D} ctx Context to set the dash line on + * @param {Array} dashArray array representing dashes + */ + _setLineDash: function(ctx, dashArray) { + if (!dashArray || dashArray.length === 0) { + return; + } + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & dashArray.length) { + dashArray.push.apply(dashArray, dashArray); + } + ctx.setLineDash(dashArray); + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + */ + _renderControls: function(ctx, styleOverride) { + var vpt = this.getViewportTransform(), + matrix = this.calcTransformMatrix(), + options, drawBorders, drawControls; + styleOverride = styleOverride || { }; + drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders; + drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls; + matrix = fabric.util.multiplyTransformMatrices(vpt, matrix); + options = fabric.util.qrDecompose(matrix); + ctx.save(); + ctx.translate(options.translateX, options.translateY); + ctx.lineWidth = 1 * this.borderScaleFactor; + if (!this.group) { + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + } + ctx.rotate(degreesToRadians(options.angle)); + if (styleOverride.forActiveSelection || this.group) { + drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); + } + else { + drawBorders && this.drawBorders(ctx, styleOverride); + } + drawControls && this.drawControls(ctx, styleOverride); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setShadow: function(ctx) { + if (!this.shadow) { + return; + } + + var shadow = this.shadow, canvas = this.canvas, scaling, + multX = (canvas && canvas.viewportTransform[0]) || 1, + multY = (canvas && canvas.viewportTransform[3]) || 1; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + if (canvas && canvas._isRetinaScaling()) { + multX *= fabric.devicePixelRatio; + multY *= fabric.devicePixelRatio; + } + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * + (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _removeShadow: function(ctx) { + if (!this.shadow) { + return; + } + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} filler fabric.Pattern or fabric.Gradient + * @return {Object} offset.offsetX offset for text rendering + * @return {Object} offset.offsetY offset for text rendering + */ + _applyPatternGradientTransform: function(ctx, filler) { + if (!filler || !filler.toLive) { + return { offsetX: 0, offsetY: 0 }; + } + var t = filler.gradientTransform || filler.patternTransform; + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; + + if (filler.gradientUnits === 'percentage') { + ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); + } + else { + ctx.transform(1, 0, 0, 1, offsetX, offsetY); + } + if (t) { + ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); + } + return { offsetX: offsetX, offsetY: offsetY }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderPaintInOrder: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderStroke(ctx); + this._renderFill(ctx); + } + else { + this._renderFill(ctx); + this._renderStroke(ctx); + } + }, + + /** + * @private + * function that actually render something on the context. + * empty here to allow Obects to work on tests to benchmark fabric functionalites + * not related to rendering + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(/* ctx */) { + + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderFill: function(ctx) { + if (!this.fill) { + return; + } + + ctx.save(); + this._setFillStyles(ctx, this); + if (this.fillRule === 'evenodd') { + ctx.fill('evenodd'); + } + else { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderStroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + if (this.strokeUniform && this.group) { + var scaling = this.getObjectScaling(); + ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); + } + else if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); + } + this._setLineDash(ctx, this.strokeDashArray); + this._setStrokeStyles(ctx, this); + ctx.stroke(); + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drwabacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Gradient} filler a fabric gradient instance + */ + _applyPatternForTransformedGradient: function(ctx, filler) { + var dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + pCanvas = fabric.util.createCanvasElement(), pCtx, retinaScaling = this.canvas.getRetinaScaling(), + width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.scale( + dims.zoomX / this.scaleX / retinaScaling, + dims.zoomY / this.scaleY / retinaScaling + ); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fillStyle = filler.toLive(ctx); + pCtx.fill(); + ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); + ctx.scale( + retinaScaling * this.scaleX / dims.zoomX, + retinaScaling * this.scaleY / dims.zoomY + ); + ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { x: this.left + this.width / 2, y: this.top + this.height / 2 }; + }, + + /** + * This function is an helper for svg import. it decompose the transformMatrix + * and assign properties to object. + * untransformed coordinates + * @private + * @chainable + */ + _assignTransformMatrixProps: function() { + if (this.transformMatrix) { + var options = fabric.util.qrDecompose(this.transformMatrix); + this.flipX = false; + this.flipY = false; + this.set('scaleX', options.scaleX); + this.set('scaleY', options.scaleY); + this.angle = options.angle; + this.skewX = options.skewX; + this.skewY = 0; + } + }, + + /** + * This function is an helper for svg import. it removes the transform matrix + * and set to object properties that fabricjs can handle + * @private + * @param {Object} preserveAspectRatioOptions + * @return {thisArg} + */ + _removeTransformMatrix: function(preserveAspectRatioOptions) { + var center = this._findCenterFromElement(); + if (this.transformMatrix) { + this._assignTransformMatrixProps(); + center = fabric.util.transformPoint(center, this.transformMatrix); + } + this.transformMatrix = null; + if (preserveAspectRatioOptions) { + this.scaleX *= preserveAspectRatioOptions.scaleX; + this.scaleY *= preserveAspectRatioOptions.scaleY; + this.cropX = preserveAspectRatioOptions.cropX; + this.cropY = preserveAspectRatioOptions.cropY; + center.x += preserveAspectRatioOptions.offsetLeft; + center.y += preserveAspectRatioOptions.offsetTop; + this.width = preserveAspectRatioOptions.width; + this.height = preserveAspectRatioOptions.height; + } + this.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * Clones an instance, using a callback method will work for every object. + * @param {Function} callback Callback is invoked with a clone as a first argument + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + */ + clone: function(callback, propertiesToInclude) { + var objectForm = this.toObject(propertiesToInclude); + if (this.constructor.fromObject) { + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); + } + }, + + /** + * Creates an instance of fabric.Image out of an object + * makes use of toCanvasElement. + * Once this method was based on toDataUrl and loadImage, so it also had a quality + * and format option. toCanvasElement is faster and produce no loss of quality. + * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. + * toCanvasElement and then toBlob from the obtained canvas is also a good option. + * This method is sync now, but still support the callback because we did not want to break. + * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. + * @param {Function} callback callback, invoked with an instance as a first argument + * @param {Object} [options] for clone as image, passed to toDataURL + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {fabric.Object} thisArg + */ + cloneAsImage: function(callback, options) { + var canvasEl = this.toCanvasElement(options); + if (callback) { + callback(new fabric.Image(canvasEl)); + } + return this; + }, + + /** + * Converts an object into a HTMLCanvas element + * @param {Object} options Options object + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {HTMLCanvasElement} Returns DOM element with the fabric.Object + */ + toCanvasElement: function(options) { + options || (options = { }); + + var utils = fabric.util, origParams = utils.saveObjectTransform(this), + originalGroup = this.group, + originalShadow = this.shadow, abs = Math.abs, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); + delete this.group; + if (options.withoutTransform) { + utils.resetObjectTransform(this); + } + if (options.withoutShadow) { + this.shadow = null; + } + + var el = fabric.util.createCanvasElement(), + // skip canvas zoom and calculate with setCoords now. + boundingRect = this.getBoundingRect(true, true), + shadow = this.shadow, scaling, + shadowOffset = { x: 0, y: 0 }, shadowBlur, + width, height; + + if (shadow) { + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + // consider non scaling shadow. + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + } + width = boundingRect.width + shadowOffset.x; + height = boundingRect.height + shadowOffset.y; + // if the current width/height is not an integer + // we need to make it so. + el.width = Math.ceil(width); + el.height = Math.ceil(height); + var canvas = new fabric.StaticCanvas(el, { + enableRetinaScaling: false, + renderOnAddRemove: false, + skipOffscreen: false, + }); + if (options.format === 'jpeg') { + canvas.backgroundColor = '#fff'; + } + this.setPositionByOrigin(new fabric.Point(canvas.width / 2, canvas.height / 2), 'center', 'center'); + + var originalCanvas = this.canvas; + canvas.add(this); + var canvasEl = canvas.toCanvasElement(multiplier || 1, options); + this.shadow = originalShadow; + this.set('canvas', originalCanvas); + if (originalGroup) { + this.group = originalGroup; + } + this.set(origParams).setCoords(); + // canvas.dispose will call image.dispose that will nullify the elements + // since this canvas is a simple element for the process, we remove references + // to objects in this way in order to avoid object trashing. + canvas._objects = []; + canvas.dispose(); + canvas = null; + + return canvasEl; + }, + + /** + * Converts an object into a data-url-like string + * @param {Object} options Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toDataURL: function(options) { + options || (options = { }); + return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); + }, + + /** + * Returns true if specified type is identical to the type of an instance + * @param {String} type Type to check against + * @return {Boolean} + */ + isType: function(type) { + return this.type === type; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance (is 1 unless subclassed) + */ + complexity: function() { + return 1; + }, + + /** + * Returns a JSON representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON + */ + toJSON: function(propertiesToInclude) { + // delegate, not alias + return this.toObject(propertiesToInclude); + }, + + /** + * Sets "angle" of an instance with centered rotation + * @param {Number} angle Angle value (in degrees) + * @return {fabric.Object} thisArg + * @chainable + */ + rotate: function(angle) { + var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; + + if (shouldCenterOrigin) { + this._setOriginToCenter(); + } + + this.set('angle', angle); + + if (shouldCenterOrigin) { + this._resetOrigin(); + } + + return this; + }, + + /** + * Centers object horizontally on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerH: function () { + this.canvas && this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object horizontally on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterH: function () { + this.canvas && this.canvas.viewportCenterObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas && this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterV: function () { + this.canvas && this.canvas.viewportCenterObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + this.canvas && this.canvas.centerObject(this); + return this; + }, + + /** + * Centers object on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenter: function () { + this.canvas && this.canvas.viewportCenterObject(this); + return this; + }, + + /** + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; + }, + + /** + * Sets canvas globalCompositeOperation for specific object + * custom composition operation for the particular object can be specified using globalCompositeOperation property + * @param {CanvasRenderingContext2D} ctx Rendering canvas context + */ + _setupCompositeOperation: function (ctx) { + if (this.globalCompositeOperation) { + ctx.globalCompositeOperation = this.globalCompositeOperation; + } + } + }); + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object); + + extend(fabric.Object.prototype, fabric.Observable); + + /** + * Defines the number of fraction digits to use when serializing object values. + * You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc. + * @static + * @memberOf fabric.Object + * @constant + * @type Number + */ + fabric.Object.NUM_FRACTION_DIGITS = 2; + + fabric.Object._fromObject = function(className, object, callback, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); + }); + }; + + /** + * Unique id used internally when creating SVG elements + * @static + * @memberOf fabric.Object + * @type Number + */ + fabric.Object.__uid = 0; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + originXOffset = { + left: -0.5, + center: 0, + right: 0.5 + }, + originYOffset = { + top: -0.5, + center: 0, + bottom: 0.5 + }; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Translates the coordinates from a set of origin to another (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { + var x = point.x, + y = point.y, + offsetX, offsetY, dim; + + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; + } + + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } + + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; + + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; + } + + return new fabric.Point(x, y); + }, + + /** + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToCenterPoint: function(point, originX, originY) { + var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); + if (this.angle) { + return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {fabric.Point} center The point which corresponds to center of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToOriginPoint: function(center, originX, originY) { + var p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + if (this.angle) { + return fabric.util.rotatePoint(p, center, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Returns the real center coordinates of the object + * @return {fabric.Point} + */ + getCenterPoint: function() { + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); + }, + + /** + * Returns the coordinates of the object based on center coordinates + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @return {fabric.Point} + */ + // getOriginPoint: function(center) { + // return this.translateToOriginPoint(center, this.originX, this.originY); + // }, + + /** + * Returns the coordinates of the object as if it has a different origin + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + getPointByOrigin: function(originX, originY) { + var center = this.getCenterPoint(); + return this.translateToOriginPoint(center, originX, originY); + }, + + /** + * Returns the point in local coordinates + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { + p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + } + else { + p = new fabric.Point(this.left, this.top); + } + + p2 = new fabric.Point(point.x, point.y); + if (this.angle) { + p2 = fabric.util.rotatePoint(p2, center, -degreesToRadians(this.angle)); + } + return p2.subtractEquals(p); + }, + + /** + * Returns the point in global coordinates + * @param {fabric.Point} The point relative to the local coordinate system + * @return {fabric.Point} + */ + // toGlobalPoint: function(point) { + // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); + // }, + + /** + * Sets the position of the object taking into consideration the object's origin + * @param {fabric.Point} pos The new position of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {void} + */ + setPositionByOrigin: function(pos, originX, originY) { + var center = this.translateToCenterPoint(pos, originX, originY), + position = this.translateToOriginPoint(center, this.originX, this.originY); + this.set('left', position.x); + this.set('top', position.y); + }, + + /** + * @param {String} to One of 'left', 'center', 'right' + */ + adjustPosition: function(to) { + var angle = degreesToRadians(this.angle), + hypotFull = this.getScaledWidth(), + xFull = fabric.util.cos(angle) * hypotFull, + yFull = fabric.util.sin(angle) * hypotFull, + offsetFrom, offsetTo; + + //TODO: this function does not consider mixed situation like top, center. + if (typeof this.originX === 'string') { + offsetFrom = originXOffset[this.originX]; + } + else { + offsetFrom = this.originX - 0.5; + } + if (typeof to === 'string') { + offsetTo = originXOffset[to]; + } + else { + offsetTo = to - 0.5; + } + this.left += xFull * (offsetTo - offsetFrom); + this.top += yFull * (offsetTo - offsetFrom); + this.setCoords(); + this.originX = to; + }, + + /** + * Sets the origin/position of the object to it's center point + * @private + * @return {void} + */ + _setOriginToCenter: function() { + this._originalOriginX = this.originX; + this._originalOriginY = this.originY; + + var center = this.getCenterPoint(); + + this.originX = 'center'; + this.originY = 'center'; + + this.left = center.x; + this.top = center.y; + }, + + /** + * Resets the origin/position of the object to it's original origin + * @private + * @return {void} + */ + _resetOrigin: function() { + var originPoint = this.translateToOriginPoint( + this.getCenterPoint(), + this._originalOriginX, + this._originalOriginY); + + this.originX = this._originalOriginX; + this.originY = this._originalOriginY; + + this.left = originPoint.x; + this.top = originPoint.y; + + this._originalOriginX = null; + this._originalOriginY = null; + }, + + /** + * @private + */ + _getLeftTopCoords: function() { + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + }, + }); + +})(); + + +(function() { + + function arrayFromCoords(coords) { + return [ + new fabric.Point(coords.tl.x, coords.tl.y), + new fabric.Point(coords.tr.x, coords.tr.y), + new fabric.Point(coords.br.x, coords.br.y), + new fabric.Point(coords.bl.x, coords.bl.y) + ]; + } + + var util = fabric.util, + degreesToRadians = util.degreesToRadians, + multiplyMatrices = util.multiplyTransformMatrices, + transformPoint = util.transformPoint; + + util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Describe object's corner position in canvas element coordinates. + * properties are depending on control keys and padding the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from the controls positionHandler and are used + * to draw and locate controls + * @memberOf fabric.Object.prototype + */ + oCoords: null, + + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are useful to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcACoords(); + * @memberOf fabric.Object.prototype + */ + aCoords: null, + + /** + * Describe object's corner position in canvas element coordinates. + * includes padding. Used of object detection. + * set and refreshed with setCoords and calcCoords. + * @memberOf fabric.Object.prototype + */ + lineCoords: null, + + /** + * storage for object transform matrix + */ + ownMatrixCache: null, + + /** + * storage for object full transform matrix + */ + matrixCache: null, + + /** + * custom controls interface + * controls are added by default_controls.js + */ + controls: { }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * @param {Boolean} absolute will return aCoords if true or lineCoords + * @return {Object} {tl, tr, br, bl} points + */ + _getCoords: function(absolute, calculate) { + if (calculate) { + return (absolute ? this.calcACoords() : this.calcLineCoords()); + } + if (!this.aCoords || !this.lineCoords) { + this.setCoords(true); + } + return (absolute ? this.aCoords : this.lineCoords); + }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * The coords are returned in an array. + * @return {Array} [tl, tr, br, bl] of points + */ + getCoords: function(absolute, calculate) { + return arrayFromCoords(this._getCoords(absolute, calculate)); + }, + + /** + * Checks if object intersects with an area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect: function(pointTL, pointBR, absolute, calculate) { + var coords = this.getCoords(absolute, calculate), + intersection = fabric.Intersection.intersectPolygonRectangle( + coords, + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject: function(other, absolute, calculate) { + var intersection = fabric.Intersection.intersectPolygonPolygon( + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); + + return intersection.status === 'Intersection' + || other.isContainedWithinObject(this, absolute, calculate) + || this.isContainedWithinObject(other, absolute, calculate); + }, + + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject: function(other, absolute, calculate) { + var points = this.getCoords(absolute, calculate), + otherCoords = absolute ? other.aCoords : other.lineCoords, + i = 0, lines = other._getImageLines(otherCoords); + for (; i < 4; i++) { + if (!other.containsPoint(points[i], lines)) { + return false; + } + } + return true; + }, + + /** + * Checks if object is fully contained within area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect: function(pointTL, pointBR, absolute, calculate) { + var boundingRect = this.getBoundingRect(absolute, calculate); + + return ( + boundingRect.left >= pointTL.x && + boundingRect.left + boundingRect.width <= pointBR.x && + boundingRect.top >= pointTL.y && + boundingRect.top + boundingRect.height <= pointBR.y + ); + }, + + /** + * Checks if point is inside the object + * @param {fabric.Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if point is inside the object + */ + containsPoint: function(point, lines, absolute, calculate) { + var coords = this._getCoords(absolute, calculate), + lines = lines || this._getImageLines(coords), + xPoints = this._findCrossPoints(point, lines); + // if xPoints is odd then point is inside the object + return (xPoints !== 0 && xPoints % 2 === 1); + }, + + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appears on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords + * @return {Boolean} true if object is fully or partially contained within canvas + */ + isOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + var points = this.getCoords(true, calculate); + // if some point is on screen, the object is on screen. + if (points.some(function(point) { + return point.x <= pointBR.x && point.x >= pointTL.x && + point.y <= pointBR.y && point.y >= pointTL.y; + })) { + return true; + } + // no points on screen, check intersection with absolute coordinates + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + return this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Checks if the object contains the midpoint between canvas extremities + * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen + * @private + * @param {Fabric.Point} pointTL Top Left point + * @param {Fabric.Point} pointBR Top Right point + * @param {Boolean} calculate use coordinates of current position instead of .oCoords + * @return {Boolean} true if the object contains the point + */ + _containsCenterOfCanvas: function(pointTL, pointBR, calculate) { + // worst case scenario the object is so big that contains the screen + var centerPoint = { x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2 }; + if (this.containsPoint(centerPoint, null, true, calculate)) { + return true; + } + return false; + }, + + /** + * Checks if object is partially contained within the canvas with current viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is partially contained within canvas + */ + isPartiallyOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + var allPointsAreOutside = this.getCoords(true, calculate).every(function(point) { + return (point.x >= pointBR.x || point.x <= pointTL.x) && + (point.y >= pointBR.y || point.y <= pointTL.y); + }); + return allPointsAreOutside && this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} oCoords Coordinates of the object corners + */ + _getImageLines: function(oCoords) { + + var lines = { + topline: { + o: oCoords.tl, + d: oCoords.tr + }, + rightline: { + o: oCoords.tr, + d: oCoords.br + }, + bottomline: { + o: oCoords.br, + d: oCoords.bl + }, + leftline: { + o: oCoords.bl, + d: oCoords.tl + } + }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; + }, + + /** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {fabric.Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + */ + // remove yi, not used but left code here just in case. + _findCrossPoints: function(point, lines) { + var b1, b2, a1, a2, xi, // yi, + xcount = 0, + iLine; + + for (var lineKey in lines) { + iLine = lines[lineKey]; + // optimisation 1: line below point. no cross + if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { + continue; + } + // optimisation 2: line above point. no cross + if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { + xi = iLine.o.x; + // yi = point.y; + } + // calculate the intersection point + else { + b1 = 0; + b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + a1 = point.y - b1 * point.x; + a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + // yi = a1 + b1 * xi; + } + // dont count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimisation 4: specific for square images + if (xcount === 2) { + break; + } + } + return xcount; + }, + + /** + * Returns coordinates of object's bounding rectangle (left, top, width, height) + * the box is intended as aligned to axis of canvas. + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect: function(absolute, calculate) { + var coords = this.getCoords(absolute, calculate); + return util.makeBoundingBoxFromPoints(coords); + }, + + /** + * Returns width of an object's bounding box counting transformations + * before 2.0 it was named getWidth(); + * @return {Number} width value + */ + getScaledWidth: function() { + return this._getTransformedDimensions().x; + }, + + /** + * Returns height of an object bounding box counting transformations + * before 2.0 it was named getHeight(); + * @return {Number} height value + */ + getScaledHeight: function() { + return this._getTransformedDimensions().y; + }, + + /** + * Makes sure the scale is valid and modifies it if necessary + * @private + * @param {Number} value + * @return {Number} + */ + _constrainScale: function(value) { + if (Math.abs(value) < this.minScaleLimit) { + if (value < 0) { + return -this.minScaleLimit; + } + else { + return this.minScaleLimit; + } + } + else if (value === 0) { + return 0.0001; + } + return value; + }, + + /** + * Scales an object (equally by x and y) + * @param {Number} value Scale factor + * @return {fabric.Object} thisArg + * @chainable + */ + scale: function(value) { + this._set('scaleX', value); + this._set('scaleY', value); + return this.setCoords(); + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New width value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).width / this.getScaledWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New height value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Calculates and returns the .coords of an object. + * unused by the library, only for the end dev. + * @return {Object} Object with tl, tr, br, bl .... + * @chainable + * @deprecated + */ + calcCoords: function(absolute) { + // this is a compatibility function to avoid removing calcCoords now. + if (absolute) { + return this.calcACoords(); + } + return this.calcOCoords(); + }, + + calcLineCoords: function() { + var vpt = this.getViewportTransform(), + padding = this.padding, angle = degreesToRadians(this.angle), + cos = util.cos(angle), sin = util.sin(angle), + cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, + cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); + + var lineCoords = { + tl: transformPoint(aCoords.tl, vpt), + tr: transformPoint(aCoords.tr, vpt), + bl: transformPoint(aCoords.bl, vpt), + br: transformPoint(aCoords.br, vpt), + }; + + if (padding) { + lineCoords.tl.x -= cosPMinusSinP; + lineCoords.tl.y -= cosPSinP; + lineCoords.tr.x += cosPSinP; + lineCoords.tr.y -= cosPMinusSinP; + lineCoords.bl.x -= cosPSinP; + lineCoords.bl.y += cosPMinusSinP; + lineCoords.br.x += cosPMinusSinP; + lineCoords.br.y += cosPSinP; + } + + return lineCoords; + }, + + calcOCoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + vpt = this.getViewportTransform(), + startMatrix = multiplyMatrices(vpt, translateMatrix), + finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), + finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), + dim = this._calculateCurrentDimensions(), + coords = {}; + this.forEachControl(function(control, key, fabricObject) { + coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); + }); + + // debug code + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // Object.keys(coords).forEach(function(key) { + // var control = coords[key]; + // canvas.contextTop.fillRect(control.x, control.y, 3, 3); + // }); + // }, 50); + return coords; + }, + + calcACoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + finalMatrix = multiplyMatrices(translateMatrix, rotateMatrix), + dim = this._getTransformedDimensions(), + w = dim.x / 2, h = dim.y / 2; + return { + // corners + tl: transformPoint({ x: -w, y: -h }, finalMatrix), + tr: transformPoint({ x: w, y: -h }, finalMatrix), + bl: transformPoint({ x: -w, y: h }, finalMatrix), + br: transformPoint({ x: w, y: h }, finalMatrix) + }; + }, + + /** + * Sets corner and controls position coordinates based on current angle, width and height, left and top. + * oCoords are used to find the corners + * aCoords are used to quickly find an object on the canvas + * lineCoords are used to quickly find object during pointer events. + * See {@link https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords|When-to-call-setCoords} + * @param {Boolean} [skipCorners] skip calculation of oCoords. + * @return {fabric.Object} thisArg + * @chainable + */ + setCoords: function(skipCorners) { + this.aCoords = this.calcACoords(); + // in case we are in a group, for how the inner group target check works, + // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. + this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); + if (skipCorners) { + return this; + } + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this.oCoords = this.calcOCoords(); + this._setCornerCoords && this._setCornerCoords(); + return this; + }, + + /** + * calculate rotation matrix of an object + * @return {Array} rotation matrix for the object + */ + _calcRotateMatrix: function() { + return util.calcRotateMatrix(this); + }, + + /** + * calculate the translation matrix for an object transform + * @return {Array} rotation matrix for the object + */ + _calcTranslateMatrix: function() { + var center = this.getCenterPoint(); + return [1, 0, 0, 1, center.x, center.y]; + }, + + transformMatrixKey: function(skipGroup) { + var sep = '_', prefix = ''; + if (!skipGroup && this.group) { + prefix = this.group.transformMatrixKey(skipGroup) + sep; + }; + return prefix + this.top + sep + this.left + sep + this.scaleX + sep + this.scaleY + + sep + this.skewX + sep + this.skewY + sep + this.angle + sep + this.originX + sep + this.originY + + sep + this.width + sep + this.height + sep + this.strokeWidth + this.flipX + this.flipY; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties. + * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations + * There are some situation in which this is useful to avoid the fake rotation. + * @return {Array} transform matrix for the object + */ + calcTransformMatrix: function(skipGroup) { + var matrix = this.calcOwnMatrix(); + if (skipGroup || !this.group) { + return matrix; + } + var key = this.transformMatrixKey(skipGroup), cache = this.matrixCache || (this.matrixCache = {}); + if (cache.key === key) { + return cache.value; + } + if (this.group) { + matrix = multiplyMatrices(this.group.calcTransformMatrix(false), matrix); + } + cache.key = key; + cache.value = matrix; + return matrix; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {Array} transform matrix for the object + */ + calcOwnMatrix: function() { + var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); + if (cache.key === key) { + return cache.value; + } + var tMatrix = this._calcTranslateMatrix(), + options = { + angle: this.angle, + translateX: tMatrix[4], + translateY: tMatrix[5], + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + flipX: this.flipX, + flipY: this.flipY, + }; + cache.key = key; + cache.value = util.composeMatrix(options); + return cache.value; + }, + + /* + * Calculate object dimensions from its properties + * @private + * @deprecated since 3.4.0, please use fabric.util._calcDimensionsTransformMatrix + * not including or including flipX, flipY to emulate the flipping boolean + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { + return util.calcDimensionsMatrix({ + skewX: skewX, + skewY: skewY, + scaleX: this.scaleX * (flipping && this.flipX ? -1 : 1), + scaleY: this.scaleY * (flipping && this.flipY ? -1 : 1) + }); + }, + + /* + * Calculate object dimensions from its properties + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getNonTransformedDimensions: function() { + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param {Number} skewX, a value to override current skewX + * @param {Number} skewY, a value to override current skewY + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions, dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + + if (this.strokeUniform) { + dimX = this.width; + dimY = this.height; + } + else { + dimensions = this._getNonTransformedDimensions(); + dimX = dimensions.x; + dimY = dimensions.y; + } + if (noSkew) { + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + } + var bbox = util.sizeAfterTransform(dimX, dimY, { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: skewX, + skewY: skewY, + }); + return this._finalizeDimensions(bbox.x, bbox.y); + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param Number width width of the bbox + * @param Number height height of the bbox + * @private + * @return {Object} .x finalized width dimension + * @return {Object} .y finalized height dimension + */ + _finalizeDimensions: function(width, height) { + return this.strokeUniform ? + { x: width + this.strokeWidth, y: height + this.strokeWidth } + : + { x: width, y: height }; + }, + + /* + * Calculate object dimensions for controls box, including padding and canvas zoom. + * and active selection + * private + */ + _calculateCurrentDimensions: function() { + var vpt = this.getViewportTransform(), + dim = this._getTransformedDimensions(), + p = transformPoint(dim, vpt, true); + return p.scalarAdd(2 * this.padding); + }, + }); +})(); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else if (this.canvas) { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else if (this.canvas) { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else if (this.canvas) { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else if (this.canvas) { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group && this.group.type !== 'activeSelection') { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else if (this.canvas) { + this.canvas.moveTo(this, index); + } + return this; + } +}); + + +/* _TO_SVG_START_ */ +(function() { + function getSvgColorString(prop, value) { + if (!value) { + return prop + ': none; '; + } + else if (value.toLive) { + return prop + ': url(#SVGID_' + value.id + '); '; + } + else { + var color = new fabric.Color(value), + str = prop + ': ' + color.toRgb() + '; ', + opacity = color.getAlpha(); + if (opacity !== 1) { + //change the color in rgb + opacity + str += prop + '-opacity: ' + opacity.toString() + '; '; + } + return str; + } + } + + var toFixed = fabric.util.toFixed; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + + var fillRule = this.fillRule ? this.fillRule : 'nonzero', + strokeWidth = this.strokeWidth ? this.strokeWidth : '0', + strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none', + strokeDashOffset = this.strokeDashOffset ? this.strokeDashOffset : '0', + strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', + strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', + strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', + opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', + visibility = this.visible ? '' : ' visibility: hidden;', + filter = skipShadow ? '' : this.getSvgFilter(), + fill = getSvgColorString('fill', this.fill), + stroke = getSvgColorString('stroke', this.stroke); + + return [ + stroke, + 'stroke-width: ', strokeWidth, '; ', + 'stroke-dasharray: ', strokeDashArray, '; ', + 'stroke-linecap: ', strokeLineCap, '; ', + 'stroke-dashoffset: ', strokeDashOffset, '; ', + 'stroke-linejoin: ', strokeLineJoin, '; ', + 'stroke-miterlimit: ', strokeMiterLimit, '; ', + fill, + 'fill-rule: ', fillRule, '; ', + 'opacity: ', opacity, ';', + filter, + visibility + ].join(''); + }, + + /** + * Returns styles-string for svg-export + * @param {Object} style the object from which to retrieve style properties + * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. + * @return {String} + */ + getSvgSpanStyles: function(style, useWhiteSpace) { + var term = '; '; + var fontFamily = style.fontFamily ? + 'font-family: ' + (((style.fontFamily.indexOf('\'') === -1 && style.fontFamily.indexOf('"') === -1) ? + '\'' + style.fontFamily + '\'' : style.fontFamily)) + term : ''; + var strokeWidth = style.strokeWidth ? 'stroke-width: ' + style.strokeWidth + term : '', + fontFamily = fontFamily, + fontSize = style.fontSize ? 'font-size: ' + style.fontSize + 'px' + term : '', + fontStyle = style.fontStyle ? 'font-style: ' + style.fontStyle + term : '', + fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + term : '', + fill = style.fill ? getSvgColorString('fill', style.fill) : '', + stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '', + textDecoration = this.getSvgTextDecoration(style), + deltaY = style.deltaY ? 'baseline-shift: ' + (-style.deltaY) + '; ' : ''; + if (textDecoration) { + textDecoration = 'text-decoration: ' + textDecoration + term; + } + + return [ + stroke, + strokeWidth, + fontFamily, + fontSize, + fontStyle, + fontWeight, + textDecoration, + fill, + deltaY, + useWhiteSpace ? 'white-space: pre; ' : '' + ].join(''); + }, + + /** + * Returns text-decoration property for svg-export + * @param {Object} style the object from which to retrieve style properties + * @return {String} + */ + getSvgTextDecoration: function(style) { + return ['overline', 'underline', 'line-through'].filter(function(decoration) { + return style[decoration.replace('-', '')]; + }).join(' '); + }, + + /** + * Returns filter for svg shadow + * @return {String} + */ + getSvgFilter: function() { + return this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + }, + + /** + * Returns id attribute for svg output + * @return {String} + */ + getSvgCommons: function() { + return [ + this.id ? 'id="' + this.id + '" ' : '', + this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '', + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @param {Boolean} use the full transform or the single object one. + * @return {String} + */ + getSvgTransform: function(full, additionalTransform) { + var transform = full ? this.calcTransformMatrix() : this.calcOwnMatrix(), + svgTransform = 'transform="' + fabric.util.matrixToSVG(transform); + return svgTransform + + (additionalTransform || '') + '" '; + }, + + _setSVGBg: function(textBgRects) { + if (this.backgroundColor) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + } + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + return this._createBaseSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + return '\t' + this._createBaseClipPathSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * @private + */ + _createBaseClipPathSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var reviver = options.reviver, + additionalTransform = options.additionalTransform || '', + commonPieces = [ + this.getSvgTransform(true, additionalTransform), + this.getSvgCommons(), + ].join(''), + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'); + objectMarkup[index] = commonPieces; + return reviver ? reviver(objectMarkup.join('')) : objectMarkup.join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var noStyle = options.noStyle, + reviver = options.reviver, + styleInfo = noStyle ? '' : 'style="' + this.getSvgStyles() + '" ', + shadowInfo = options.withShadow ? 'style="' + this.getSvgFilter() + '" ' : '', + clipPath = this.clipPath, + vectorEffect = this.strokeUniform ? 'vector-effect="non-scaling-stroke" ' : '', + absoluteClipPath = clipPath && clipPath.absolutePositioned, + stroke = this.stroke, fill = this.fill, shadow = this.shadow, + commonPieces, markup = [], clipPathMarkup, + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'), + additionalTransform = options.additionalTransform; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + clipPathMarkup = '\n' + + clipPath.toClipPathSVG(reviver) + + '\n'; + } + if (absoluteClipPath) { + markup.push( + '\n' + ); + } + markup.push( + '\n' + ); + commonPieces = [ + styleInfo, + vectorEffect, + noStyle ? '' : this.addPaintOrder(), ' ', + additionalTransform ? 'transform="' + additionalTransform + '" ' : '', + ].join(''); + objectMarkup[index] = commonPieces; + if (fill && fill.toLive) { + markup.push(fill.toSVG(this)); + } + if (stroke && stroke.toLive) { + markup.push(stroke.toSVG(this)); + } + if (shadow) { + markup.push(shadow.toSVG(this)); + } + if (clipPath) { + markup.push(clipPathMarkup); + } + markup.push(objectMarkup.join('')); + markup.push('\n'); + absoluteClipPath && markup.push('\n'); + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + addPaintOrder: function() { + return this.paintFirst !== 'fill' ? ' paint-order="' + this.paintFirst + '" ' : ''; + } + }); +})(); +/* _TO_SVG_END_ */ + + +(function() { + + var extend = fabric.util.object.extend, + originalSet = 'stateProperties'; + + /* + Depends on `stateProperties` + */ + function saveProps(origin, destination, props) { + var tmpObj = { }, deep = true; + props.forEach(function(prop) { + tmpObj[prop] = origin[prop]; + }); + + extend(origin[destination], tmpObj, deep); + } + + function _isEqual(origValue, currentValue, firstPass) { + if (origValue === currentValue) { + // if the objects are identical, return + return true; + } + else if (Array.isArray(origValue)) { + if (!Array.isArray(currentValue) || origValue.length !== currentValue.length) { + return false; + } + for (var i = 0, len = origValue.length; i < len; i++) { + if (!_isEqual(origValue[i], currentValue[i])) { + return false; + } + } + return true; + } + else if (origValue && typeof origValue === 'object') { + var keys = Object.keys(origValue), key; + if (!currentValue || + typeof currentValue !== 'object' || + (!firstPass && keys.length !== Object.keys(currentValue).length) + ) { + return false; + } + for (var i = 0, len = keys.length; i < len; i++) { + key = keys[i]; + // since clipPath is in the statefull cache list and the clipPath objects + // would be iterated as an object, this would lead to possible infinite recursion + // we do not want to compare those. + if (key === 'canvas' || key === 'group') { + continue; + } + if (!_isEqual(origValue[key], currentValue[key])) { + return false; + } + } + return true; + } + } + + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns true if object state (one of its state properties) was changed + * @param {String} [propertySet] optional name for the set of property we want to save + * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called + */ + hasStateChanged: function(propertySet) { + propertySet = propertySet || originalSet; + var dashedPropertySet = '_' + propertySet; + if (Object.keys(this[dashedPropertySet]).length < this[propertySet].length) { + return true; + } + return !_isEqual(this[dashedPropertySet], this, true); + }, + + /** + * Saves state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + saveState: function(options) { + var propertySet = options && options.propertySet || originalSet, + destination = '_' + propertySet; + if (!this[destination]) { + return this.setupState(options); + } + saveProps(this, destination, this[propertySet]); + if (options && options.stateProperties) { + saveProps(this, destination, options.stateProperties); + } + return this; + }, + + /** + * Setups state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + setupState: function(options) { + options = options || { }; + var propertySet = options.propertySet || originalSet; + options.propertySet = propertySet; + this['_' + propertySet] = { }; + this.saveState(options); + return this; + } + }); +})(); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Determines which corner has been clicked + * @private + * @param {Object} pointer The pointer indicating the mouse position + * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found + */ + _findTargetCorner: function(pointer, forTouch) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { + return false; + } + + var ex = pointer.x, + ey = pointer.y, + xPoints, + lines, keys = Object.keys(this.oCoords), + j = keys.length - 1, i; + this.__corner = 0; + + // cycle in reverse order so we pick first the one on top + for (; j >= 0; j--) { + i = keys[j]; + if (!this.isControlVisible(i)) { + continue; + } + + lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); + // // debugging + // + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); + if (xPoints !== 0 && xPoints % 2 === 1) { + this.__corner = i; + return i; + } + } + return false; + }, + + /** + * Calls a function for each control. The function gets called, + * with the control, the object that is calling the iterator and the control's key + * @param {Function} fn function to iterate over the controls over + */ + forEachControl: function(fn) { + for (var i in this.controls) { + fn(this.controls[i], i, this); + }; + }, + + /** + * Sets the coordinates of the draggable boxes in the corners of + * the image used to scale/rotate it. + * note: if we would switch to ROUND corner area, all of this would disappear. + * everything would resolve to a single point and a pythagorean theorem for the distance + * @private + */ + _setCornerCoords: function() { + var coords = this.oCoords; + + for (var control in coords) { + var controlObject = this.controls[control]; + coords[control].corner = controlObject.calcCornerCoords( + this.angle, this.cornerSize, coords[control].x, coords[control].y, false); + coords[control].touchCorner = controlObject.calcCornerCoords( + this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true); + } + }, + + /** + * Draws a colored layer behind the object, inside its selection borders. + * Requires public options: padding, selectionBackgroundColor + * this function is called when the context is transformed + * has checks to be skipped when the object is on a staticCanvas + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawSelectionBackground: function(ctx) { + if (!this.selectionBackgroundColor || + (this.canvas && !this.canvas.interactive) || + (this.canvas && this.canvas._activeObject !== this) + ) { + return this; + } + ctx.save(); + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + vpt = this.canvas.viewportTransform; + ctx.translate(center.x, center.y); + ctx.scale(1 / vpt[0], 1 / vpt[3]); + ctx.rotate(degreesToRadians(this.angle)); + ctx.fillStyle = this.selectionBackgroundColor; + ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBorders: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + var wh = this._calculateCurrentDimensions(), + strokeWidth = this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls, + shouldStroke = false; + + ctx.save(); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + if (hasControls) { + ctx.beginPath(); + this.forEachControl(function(control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + if (shouldStroke) { + ctx.stroke(); + } + } + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box when it is inside a group. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {object} options object representing current object parameters + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBordersInGroup: function(ctx, options, styleOverride) { + styleOverride = styleOverride || {}; + var bbox = fabric.util.sizeAfterTransform(this.width, this.height, options), + strokeWidth = this.strokeWidth, + strokeUniform = this.strokeUniform, + borderScaleFactor = this.borderScaleFactor, + width = + bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, + height = + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; + ctx.save(); + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + ctx.restore(); + return this; + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height + * Requires public options: cornerSize, padding + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawControls: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + ctx.save(); + var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; + ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); + ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; + if (!this.transparentCorners) { + ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; + } + this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); + this.setCoords(); + if (this.group) { + // fabricJS does not really support drawing controls inside groups, + // this piece of code here helps having at least the control in places. + // If an application needs to show some objects as selected because of some UI state + // can still call Object._renderControls() on any object they desire, independently of groups. + // using no padding, circular controls and hiding the rotating cursor is higly suggested, + matrix = this.group.calcTransformMatrix(); + } + this.forEachControl(function(control, key, fabricObject) { + p = fabricObject.oCoords[key]; + if (control.getVisibility(fabricObject, key)) { + if (matrix) { + p = fabric.util.transformPoint(p, matrix); + } + control.render(ctx, p.x, p.y, styleOverride, fabricObject); + } + }); + ctx.restore(); + + return this; + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlKey) { + return this.controls[controlKey] && this.controls[controlKey].getVisibility(this, controlKey); + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlKey, visible) { + if (!this._controlsVisibility) { + this._controlsVisibility = {}; + } + this._controlsVisibility[controlKey] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to deselect this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onDeselect: function() { + // implemented by sub-classes, as needed. + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to select this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onSelect: function() { + // implemented by sub-classes, as needed. + } + }); +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Animation duration (in ms) for fx* methods + * @type Number + * @default + */ + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectH: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.left, + endValue: this.getCenter().left, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('left', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Centers object vertically with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectV: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.top, + endValue: this.getCenter().top, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('top', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @param {fabric.Object} object Object to remove + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.opacity, + endValue: 0, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('opacity', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Animates object's properties + * @param {String|Object} property Property to animate (if string) or properties to animate (if object) + * @param {Number|Object} value Value to animate property to (if string was given first) or options object + * @return {fabric.Object} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation} + * @chainable + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + var propsToAnimate = [], prop, skipCallbacks; + for (prop in arguments[0]) { + propsToAnimate.push(prop); + } + for (var i = 0, len = propsToAnimate.length; i < len; i++) { + prop = propsToAnimate[i]; + skipCallbacks = i !== len - 1; + this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @param {String} property Property to animate + * @param {String} to Value to animate to + * @param {Object} [options] Options object + * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked + */ + _animate: function(property, to, options, skipCallbacks) { + var _this = this, propPair; + + to = to.toString(); + + if (!options) { + options = { }; + } + else { + options = fabric.util.object.clone(options); + } + + if (~property.indexOf('.')) { + propPair = property.split('.'); + } + + var propIsColor = + _this.colorProperties.indexOf(property) > -1 || + (propPair && _this.colorProperties.indexOf(propPair[1]) > -1); + + var currentValue = propPair + ? this.get(propPair[0])[propPair[1]] + : this.get(property); + + if (!('from' in options)) { + options.from = currentValue; + } + + if (!propIsColor) { + if (~to.indexOf('=')) { + to = currentValue + parseFloat(to.replace('=', '')); + } + else { + to = parseFloat(to); + } + } + + var _options = { + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + abort: options.abort && function(value, valueProgress, timeProgress) { + return options.abort.call(_this, value, valueProgress, timeProgress); + }, + onChange: function (value, valueProgress, timeProgress) { + if (propPair) { + _this[propPair[0]][propPair[1]] = value; + } + else { + _this.set(property, value); + } + if (skipCallbacks) { + return; + } + options.onChange && options.onChange(value, valueProgress, timeProgress); + }, + onComplete: function (value, valueProgress, timeProgress) { + if (skipCallbacks) { + return; + } + + _this.setCoords(); + options.onComplete && options.onComplete(value, valueProgress, timeProgress); + } + }; + + if (propIsColor) { + return fabric.util.animateColor(_options.startValue, _options.endValue, _options.duration, _options); + } + else { + return fabric.util.animate(_options); + } + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }; + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * Line class + * @class fabric.Line + * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'line', + + /** + * x value or first line edge + * @type Number + * @default + */ + x1: 0, + + /** + * y value or first line edge + * @type Number + * @default + */ + y1: 0, + + /** + * x value or second line edge + * @type Number + * @default + */ + x2: 0, + + /** + * y value or second line edge + * @type Number + * @default + */ + y2: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('x1', 'x2', 'y1', 'y2'), + + /** + * Constructor + * @param {Array} [points] Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} thisArg + */ + initialize: function(points, options) { + if (!points) { + points = [0, 0, 0, 0]; + } + + this.callSuper('initialize', options); + + this.set('x1', points[0]); + this.set('y1', points[1]); + this.set('x2', points[2]); + this.set('y2', points[3]); + + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Object} [options] Options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.width = Math.abs(this.x2 - this.x1); + this.height = Math.abs(this.y2 - this.y1); + + this.left = 'left' in options + ? options.left + : this._getLeftToOriginX(); + + this.top = 'top' in options + ? options.top + : this._getTopToOriginY(); + }, + + /** + * @private + * @param {String} key + * @param {*} value + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + if (typeof coordProps[key] !== 'undefined') { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. + */ + _getLeftToOriginX: makeEdgeToOriginGetter( + { // property names + origin: 'originX', + axis1: 'x1', + axis2: 'x2', + dimension: 'width' + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right' + } + ), + + /** + * @private + * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. + */ + _getTopToOriginY: makeEdgeToOriginGetter( + { // property names + origin: 'originY', + axis1: 'y1', + axis2: 'y2', + dimension: 'height' + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom' + } + ), + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + + var p = this.calcLinePoints(); + ctx.moveTo(p.x1, p.y1); + ctx.lineTo(p.x2, p.y2); + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = this.stroke || ctx.fillStyle; + this.stroke && this._renderStroke(ctx); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); + }, + + /* + * Calculate object dimensions from its properties + * @private + */ + _getNonTransformedDimensions: function() { + var dim = this.callSuper('_getNonTransformedDimensions'); + if (this.strokeLineCap === 'butt') { + if (this.width === 0) { + dim.y -= this.strokeWidth; + } + if (this.height === 0) { + dim.x -= this.strokeWidth; + } + } + return dim; + }, + + /** + * Recalculates line points given width and height + * @private + */ + calcLinePoints: function() { + var xMult = this.x1 <= this.x2 ? -1 : 1, + yMult = this.y1 <= this.y2 ? -1 : 1, + x1 = (xMult * this.width * 0.5), + y1 = (yMult * this.height * 0.5), + x2 = (xMult * this.width * -0.5), + y2 = (yMult * this.height * -0.5); + + return { + x1: x1, + x2: x2, + y1: y1, + y2: y2 + }; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var p = this.calcLinePoints(); + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) + * @static + * @memberOf fabric.Line + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @memberOf fabric.Line + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} [callback] callback function invoked after parsing + */ + fabric.Line.fromElement = function(element, callback, options) { + options = options || { }; + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), + points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + callback(new fabric.Line(points, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Line instance from an object representation + * @static + * @memberOf fabric.Line + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Line.fromObject = function(object, callback) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; + var options = clone(object, true); + options.points = [object.x1, object.y1, object.x2, object.y2]; + fabric.Object._fromObject('Line', options, _callback, 'points'); + }; + + /** + * Produces a function that calculates distance from canvas edge to Line origin. + */ + function makeEdgeToOriginGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + axis1 = propertyNames.axis1, + axis2 = propertyNames.axis2, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + switch (this.get(origin)) { + case nearest: + return Math.min(this.get(axis1), this.get(axis2)); + case center: + return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); + case farthest: + return Math.max(this.get(axis1), this.get(axis2)); + } + }; + + } + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + pi = Math.PI; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * Circle class + * @class fabric.Circle + * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'circle', + + /** + * Radius of this circle + * @type Number + * @default + */ + radius: 0, + + /** + * Start angle of the circle, moving clockwise + * deprecated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 0 + */ + startAngle: 0, + + /** + * End angle of the circle + * deprecated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 2Pi + */ + endAngle: pi * 2, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('radius', 'startAngle', 'endAngle'), + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Circle} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (key === 'radius') { + this.setRadius(value); + } + + return this; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['radius', 'startAngle', 'endAngle'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString, x = 0, y = 0, + angle = (this.endAngle - this.startAngle) % ( 2 * pi); + + if (angle === 0) { + svgString = [ + '\n' + ]; + } + else { + var startX = fabric.util.cos(this.startAngle) * this.radius, + startY = fabric.util.sin(this.startAngle) * this.radius, + endX = fabric.util.cos(this.endAngle) * this.radius, + endY = fabric.util.sin(this.endAngle) * this.radius, + largeFlag = angle > pi ? '1' : '0'; + svgString = [ + '\n' + ]; + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.arc( + 0, + 0, + this.radius, + this.startAngle, + this.endAngle, false); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @return {fabric.Circle} thisArg + */ + setRadius: function(value) { + this.radius = value; + return this.set('width', value * 2).set('height', value * 2); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @memberOf fabric.Circle + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @memberOf fabric.Circle + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @param {Object} [options] Options object + * @throws {Error} If value of `r` attribute is missing or invalid + */ + fabric.Circle.fromElement = function(element, callback) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + + if (!isValidRadius(parsedAttributes)) { + throw new Error('value of `r` attribute is required and can not be negative'); + } + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.radius; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.radius; + callback(new fabric.Circle(parsedAttributes)); + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius >= 0)); + } + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @memberOf fabric.Circle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {void} + */ + fabric.Circle.fromObject = function(object, callback) { + fabric.Object._fromObject('Circle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * Triangle class + * @class fabric.Triangle + * @extends fabric.Object + * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'triangle', + + /** + * Width is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + width: 100, + + /** + * Height is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + height: 100, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + ctx.moveTo(-widthBy2, heightBy2); + ctx.lineTo(0, -heightBy2); + ctx.lineTo(widthBy2, heightBy2); + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2, + points = [ + -widthBy2 + ' ' + heightBy2, + '0 ' + -heightBy2, + widthBy2 + ' ' + heightBy2 + ].join(','); + return [ + '' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Triangle} instance from an object representation + * @static + * @memberOf fabric.Triangle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * Ellipse class + * @class fabric.Ellipse + * @extends fabric.Object + * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'ellipse', + + /** + * Horizontal radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Ellipse} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this.set('rx', options && options.rx || 0); + this.set('ry', options && options.ry || 0); + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Ellipse} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + switch (key) { + + case 'rx': + this.rx = value; + this.set('width', value * 2); + break; + + case 'ry': + this.ry = value; + this.set('height', value * 2); + break; + + } + return this; + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRx: function() { + return this.get('rx') * this.get('scaleX'); + }, + + /** + * Returns Vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRy: function() { + return this.get('ry') * this.get('scaleY'); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.save(); + ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); + ctx.arc( + 0, + 0, + this.rx, + 0, + piBy2, + false); + ctx.restore(); + this._renderPaintInOrder(ctx); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @memberOf fabric.Ellipse + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @memberOf fabric.Ellipse + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, callback) { + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.rx; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.ry; + callback(new fabric.Ellipse(parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Ellipse} instance from an object representation + * @static + * @memberOf fabric.Ellipse + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {void} + */ + fabric.Ellipse.fromObject = function(object, callback) { + fabric.Object._fromObject('Ellipse', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Rect) { + fabric.warn('fabric.Rect is already defined'); + return; + } + + /** + * Rectangle class + * @class fabric.Rect + * @extends fabric.Object + * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { + + /** + * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('rx', 'ry'), + + /** + * Type of an object + * @type String + * @default + */ + type: 'rect', + + /** + * Horizontal border radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical border radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Initializes rx/ry attributes + * @private + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + + // 1x1 case (used in spray brush) optimization was removed because + // with caching and higher zoom level this makes more damage than help + + var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, + ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, + w = this.width, + h = this.height, + x = -this.width / 2, + y = -this.height / 2, + isRounded = rx !== 0 || ry !== 0, + /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ + k = 1 - 0.5522847498; + ctx.beginPath(); + + ctx.moveTo(x + rx, y); + + ctx.lineTo(x + w - rx, y); + isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); + + ctx.lineTo(x + w, y + h - ry); + isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); + + ctx.lineTo(x + rx, y + h); + isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); + + ctx.lineTo(x, y + ry); + isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); + + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var x = -this.width / 2, y = -this.height / 2; + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + * @memberOf fabric.Rect + * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement + */ + fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); + + /** + * Returns {@link fabric.Rect} instance from an SVG element + * @static + * @memberOf fabric.Rect + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Rect.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + options = options || { }; + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + parsedAttributes.height = parsedAttributes.height || 0; + parsedAttributes.width = parsedAttributes.width || 0; + var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect.visible = rect.visible && rect.width > 0 && rect.height > 0; + callback(rect); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Rect} instance from an object representation + * @static + * @memberOf fabric.Rect + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + */ + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * Polyline class + * @class fabric.Polyline + * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polyline', + + /** + * Points array + * @type Array + * @default + */ + points: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), + + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); + */ + initialize: function(points, options) { + options = options || {}; + this.points = points || []; + this.callSuper('initialize', options); + this._setPositionDimensions(options); + }, + + _setPositionDimensions: function(options) { + var calcDim = this._calcDimensions(options), correctLeftTop; + this.width = calcDim.width; + this.height = calcDim.height; + if (!options.fromSVG) { + correctLeftTop = this.translateToGivenOrigin( + { x: calcDim.left - this.strokeWidth / 2, y: calcDim.top - this.strokeWidth / 2 }, + 'left', + 'top', + this.originX, + this.originY + ); + } + if (typeof options.left === 'undefined') { + this.left = options.fromSVG ? calcDim.left : correctLeftTop.x; + } + if (typeof options.top === 'undefined') { + this.top = options.fromSVG ? calcDim.top : correctLeftTop.y; + } + this.pathOffset = { + x: calcDim.left + this.width / 2, + y: calcDim.top + this.height / 2 + }; + }, + + /** + * Calculate the polygon min and max point from points array, + * returning an object with left, top, width, height to measure the + * polygon size + * @return {Object} object.left X coordinate of the polygon leftmost point + * @return {Object} object.top Y coordinate of the polygon topmost point + * @return {Object} object.width distance between X coordinates of the polygon leftmost and rightmost point + * @return {Object} object.height distance between Y coordinates of the polygon topmost and bottommost point + * @private + */ + _calcDimensions: function() { + + var points = this.points, + minX = min(points, 'x') || 0, + minY = min(points, 'y') || 0, + maxX = max(points, 'x') || 0, + maxY = max(points, 'y') || 0, + width = (maxX - minX), + height = (maxY - minY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + points: this.points.concat() + }); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var points = [], diffX = this.pathOffset.x, diffY = this.pathOffset.y, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push( + toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', + toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' + ); + } + return [ + '<' + this.type + ' ', 'COMMON_PARTS', + 'points="', points.join(''), + '" />\n' + ]; + }, + /* _TO_SVG_END_ */ + + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + commonRender: function(ctx) { + var point, len = this.points.length, + x = this.pathOffset.x, + y = this.pathOffset.y; + + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return false; + } + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (var i = 0; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + return true; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + this._renderPaintInOrder(ctx); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.get('points').length; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) + * @static + * @memberOf fabric.Polyline + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @memberOf fabric.Polyline + * @param {SVGElement} element Element to parser + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polyline.fromElementGenerator = function(_class) { + return function(element, callback, options) { + if (!element) { + return callback(null); + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric[_class].ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric[_class](points, extend(parsedAttributes, options))); + }; + }; + + fabric.Polyline.fromElement = fabric.Polyline.fromElementGenerator('Polyline'); + + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @memberOf fabric.Polyline + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * Polygon class + * @class fabric.Polygon + * @extends fabric.Polyline + * @see {@link fabric.Polygon#initialize} for constructor definition + */ + fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polygon', + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + ctx.closePath(); + this._renderPaintInOrder(ctx); + }, + + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @memberOf fabric.Polygon + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns {@link fabric.Polygon} instance from an SVG element + * @static + * @memberOf fabric.Polygon + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @memberOf fabric.Polygon + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @return {void} + */ + fabric.Polygon.fromObject = function(object, callback) { + fabric.Object._fromObject('Polygon', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max, + extend = fabric.util.object.extend, + _toString = Object.prototype.toString, + toFixed = fabric.util.toFixed; + + if (fabric.Path) { + fabric.warn('fabric.Path is already defined'); + return; + } + + /** + * Path class + * @class fabric.Path + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition + */ + fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path', + + /** + * Array of path points + * @type Array + * @default + */ + path: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('path', 'fillRule'), + + stateProperties: fabric.Object.prototype.stateProperties.concat('path'), + + /** + * Constructor + * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) + * @param {Object} [options] Options object + * @return {fabric.Path} thisArg + */ + initialize: function(path, options) { + options = options || { }; + this.callSuper('initialize', options); + if (!path) { + path = []; + } + + var fromArray = _toString.call(path) === '[object Array]'; + + this.path = fabric.util.makePathSimpler( + fromArray ? path : fabric.util.parsePath(path) + ); + + if (!this.path) { + return; + } + fabric.Polyline.prototype._setPositionDimensions.call(this, options); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _renderPathCommands: function(ctx) { + var current, // current instruction + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + l = -this.pathOffset.x, + t = -this.pathOffset.y; + + ctx.beginPath(); + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'C': // bezierCurveTo, absolute + x = current[5]; + y = current[6]; + controlX = current[3]; + controlY = current[4]; + ctx.bezierCurveTo( + current[1] + l, + current[2] + t, + controlX + l, + controlY + t, + x + l, + y + t + ); + break; + + case 'Q': // quadraticCurveTo, absolute + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + current[3] + l, + current[4] + t + ); + x = current[3]; + y = current[4]; + controlX = current[1]; + controlY = current[2]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + ctx.closePath(); + break; + } + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _render: function(ctx) { + this._renderPathCommands(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns string representation of an instance + * @return {String} string representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + path: this.path.map(function(item) { return item.slice(); }), + }); + }, + + /** + * Returns dataless object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var o = this.toObject(['sourcePath'].concat(propertiesToInclude)); + if (o.sourcePath) { + delete o.path; + } + return o; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var path = fabric.util.joinPath(this.path); + return [ + '\n' + ]; + }, + + _getOffsetTransform: function() { + var digits = fabric.Object.NUM_FRACTION_DIGITS; + return ' translate(' + toFixed(-this.pathOffset.x, digits) + ', ' + + toFixed(-this.pathOffset.y, digits) + ')'; + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return '\t' + this._createBaseClipPathSVGMarkup( + this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform } + ); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return this._createBaseSVGMarkup(this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform }); + }, + /* _TO_SVG_END_ */ + + /** + * Returns number representation of an instance complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + */ + _calcDimensions: function() { + + var aX = [], + aY = [], + current, // current instruction + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + bounds; + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + bounds = []; + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'C': // bezierCurveTo, absolute + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + x = current[5]; + y = current[6]; + break; + + case 'Q': // quadraticCurveTo, absolute + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + current[1], + current[2], + current[3], + current[4] + ); + x = current[3]; + y = current[4]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + break; + } + bounds.forEach(function (point) { + aX.push(point.x); + aY.push(point.y); + }); + aX.push(x); + aY.push(y); + } + + var minX = min(aX) || 0, + minY = min(aY) || 0, + maxX = max(aX) || 0, + maxY = max(aY) || 0, + deltaX = maxX - minX, + deltaY = maxY - minY; + + return { + left: minX, + top: minY, + width: deltaX, + height: deltaY + }; + } + }); + + /** + * Creates an instance of fabric.Path from an object + * @static + * @memberOf fabric.Path + * @param {Object} object + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.sourcePath === 'string') { + var pathUrl = object.sourcePath; + fabric.loadSVGFromURL(pathUrl, function (elements) { + var path = elements[0]; + path.setOptions(object); + callback && callback(path); + }); + } + else { + fabric.Object._fromObject('Path', object, callback, 'path'); + } + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @memberOf fabric.Path + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); + + /** + * Creates an instance of fabric.Path from an SVG element + * @static + * @memberOf fabric.Path + * @param {SVGElement} element to parse + * @param {Function} callback Callback to invoke when an fabric.Path instance is created + * @param {Object} [options] Options object + * @param {Function} [callback] Options callback invoked after parsing is finished + */ + fabric.Path.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max; + + if (fabric.Group) { + return; + } + + /** + * Group class + * @class fabric.Group + * @extends fabric.Object + * @mixes fabric.Collection + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.Group#initialize} for constructor definition + */ + fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'group', + + /** + * Width of stroke + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets + * @type Boolean + * @default + */ + subTargetCheck: false, + + /** + * Groups are container, do not render anything on theyr own, ence no cache properties + * @type Array + * @default + */ + cacheProperties: [], + + /** + * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still + * available setting this boolean to true. + * @type Boolean + * @since 2.0.0 + * @default + */ + useSetOnGroup: false, + + /** + * Constructor + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. + * @return {Object} thisArg + */ + initialize: function(objects, options, isAlreadyGrouped) { + options = options || {}; + this._objects = []; + // if objects enclosed in a group have been grouped already, + // we cannot change properties of objects. + // Thus we need to set options to group without objects, + isAlreadyGrouped && this.callSuper('initialize', options); + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (!isAlreadyGrouped) { + var center = options && options.centerPoint; + // we want to set origins before calculating the bounding box. + // so that the topleft can be set with that in mind. + // if specific top and left are passed, are overwritten later + // with the callSuper('initialize', options) + if (options.originX !== undefined) { + this.originX = options.originX; + } + if (options.originY !== undefined) { + this.originY = options.originY; + } + // if coming from svg i do not want to calc bounds. + // i assume width and height are passed along options + center || this._calcBounds(); + this._updateObjectsCoords(center); + delete options.centerPoint; + this.callSuper('initialize', options); + } + else { + this._updateObjectsACoords(); + } + + this.setCoords(); + }, + + /** + * @private + */ + _updateObjectsACoords: function() { + var skipControls = true; + for (var i = this._objects.length; i--; ){ + this._objects[i].setCoords(skipControls); + } + }, + + /** + * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + */ + _updateObjectsCoords: function(center) { + var center = center || this.getCenterPoint(); + for (var i = this._objects.length; i--; ){ + this._updateObjectCoords(this._objects[i], center); + } + }, + + /** + * @private + * @param {Object} object + * @param {fabric.Point} center, current center of group. + */ + _updateObjectCoords: function(object, center) { + var objectLeft = object.left, + objectTop = object.top, + skipControls = true; + + object.set({ + left: objectLeft - center.x, + top: objectTop - center.y + }); + object.group = this; + object.setCoords(skipControls); + }, + + /** + * Returns string represenation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Adds an object to a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + var nested = !!this.group; + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + if (object) { + if (nested) { + // if this group is inside another group, we need to pre transform the object + fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); + } + this._objects.push(object); + object.group = this; + object._set('canvas', this.canvas); + } + this._calcBounds(); + this._updateObjectsCoords(); + this.dirty = true; + if (nested) { + this.group.addWithUpdate(); + } + else { + this.setCoords(); + } + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + this.setCoords(); + this.dirty = true; + return this; + }, + + /** + * @private + */ + _onObjectAdded: function(object) { + this.dirty = true; + object.group = this; + object._set('canvas', this.canvas); + }, + + /** + * @private + */ + _onObjectRemoved: function(object) { + this.dirty = true; + delete object.group; + }, + + /** + * @private + */ + _set: function(key, value) { + var i = this._objects.length; + if (this.useSetOnGroup) { + while (i--) { + this._objects[i].setOnGroup(key, value); + } + } + if (key === 'canvas') { + while (i--) { + this._objects[i]._set(key, value); + } + } + fabric.Object.prototype._set.call(this, key, value); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + var _includeDefaultValues = this.includeDefaultValues; + var objsToObject = this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .map(function (obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Returns object representation of an instance, in dataless mode. + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var objsToObject, sourcePath = this.sourcePath; + if (sourcePath) { + objsToObject = sourcePath; + } + else { + var _includeDefaultValues = this.includeDefaultValues; + objsToObject = this._objects.map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toDatalessObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + } + var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx) { + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group is already cached. + * @return {Boolean} + */ + shouldCache: function() { + var ownCache = fabric.Object.prototype.shouldCache.call(this); + if (ownCache) { + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + this.ownCaching = false; + return false; + } + } + } + return ownCache; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (fabric.Object.prototype.willDrawShadow.call(this)) { + return true; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return this.ownCaching || (this.group && this.group.isOnACache()); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].render(ctx); + } + this._drawClipPath(ctx); + }, + + /** + * Check if cache is dirty + */ + isCacheDirty: function(skipCanvas) { + if (this.callSuper('isCacheDirty', skipCanvas)) { + return true; + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].isCacheDirty(true)) { + if (this._cacheCanvas) { + // if this group has not a cache canvas there is nothing to clean + var x = this.cacheWidth / this.zoomX, y = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-x / 2, -y / 2, x, y); + } + return true; + } + } + return false; + }, + + /** + * Restores original state of each of group objects (original state is that which was before group was created). + * if the nested boolean is true, the original state will be restored just for the + * first group and not for all the group chain + * @private + * @param {Boolean} nested tell the function to restore object state up to the parent group and not more + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + var groupMatrix = this.calcOwnMatrix(); + this._objects.forEach(function(object) { + // instead of using _this = this; + fabric.util.addTransformToObject(object, groupMatrix); + delete object.group; + object.setCoords(); + }); + return this; + }, + + /** + * Realises the transform from this group onto the supplied object + * i.e. it tells you what would happen if the supplied object was in + * the group, and then the group was destroyed. It mutates the supplied + * object. + * Warning: this method is not useful anymore, it has been kept to no break the api. + * is not used in the fabricJS codebase + * this method will be reduced to using the utility. + * @private + * @deprecated + * @param {fabric.Object} object + * @param {Array} parentMatrix parent transformation + * @return {fabric.Object} transformedObject + */ + realizeTransform: function(object, parentMatrix) { + fabric.util.addTransformToObject(object, parentMatrix); + return object; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + // when group is destroyed objects needs to get a repaint to be eventually + // displayed on canvas. + this._objects.forEach(function(object) { + object.set('dirty', true); + }); + return this._restoreObjectsState(); + }, + + /** + * make a group an active selection, remove the group from canvas + * the group has to be on canvas for this to work. + * @return {fabric.ActiveSelection} thisArg + * @chainable + */ + toActiveSelection: function() { + if (!this.canvas) { + return; + } + var objects = this._objects, canvas = this.canvas; + this._objects = []; + var options = this.toObject(); + delete options.objects; + var activeSelection = new fabric.ActiveSelection([]); + activeSelection.set(options); + activeSelection.type = 'activeSelection'; + canvas.remove(this); + objects.forEach(function(object) { + object.group = activeSelection; + object.dirty = true; + canvas.add(object); + }); + activeSelection.canvas = canvas; + activeSelection._objects = objects; + canvas._activeObject = activeSelection; + activeSelection.setCoords(); + return activeSelection; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + ungroupOnCanvas: function() { + return this._restoreObjectsState(); + }, + + /** + * Sets coordinates of all objects inside group + * @return {fabric.Group} thisArg + * @chainable + */ + setObjectsCoords: function() { + var skipControls = true; + this.forEachObject(function(object) { + object.setCoords(skipControls); + }); + return this; + }, + + /** + * @private + */ + _calcBounds: function(onlyWidthHeight) { + var aX = [], + aY = [], + o, prop, coords, + props = ['tr', 'br', 'bl', 'tl'], + i = 0, iLen = this._objects.length, + j, jLen = props.length; + + for ( ; i < iLen; ++i) { + o = this._objects[i]; + coords = o.calcACoords(); + for (j = 0; j < jLen; j++) { + prop = props[j]; + aX.push(coords[prop].x); + aY.push(coords[prop].y); + } + o.aCoords = coords; + } + + this._getBounds(aX, aY, onlyWidthHeight); + }, + + /** + * @private + */ + _getBounds: function(aX, aY, onlyWidthHeight) { + var minXY = new fabric.Point(min(aX), min(aY)), + maxXY = new fabric.Point(max(aX), max(aY)), + top = minXY.y || 0, left = minXY.x || 0, + width = (maxXY.x - minXY.x) || 0, + height = (maxXY.y - minXY.y) || 0; + this.width = width; + this.height = height; + if (!onlyWidthHeight) { + // the bounding box always finds the topleft most corner. + // whatever is the group origin, we set up here the left/top position. + this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); + } + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function(reviver) { + var svgString = ['\n']; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; + }, + + /** + * Returns styles-string for svg-export, specific version for group + * @return {String} + */ + getSvgStyles: function() { + var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? + 'opacity: ' + this.opacity + ';' : '', + visibility = this.visible ? '' : ' visibility: hidden;'; + return [ + opacity, + this.getSvgFilter(), + visibility + ].join(''); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var svgString = []; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); + } + + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Group} instance from an object representation + * @static + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an group instance is created + */ + fabric.Group.fromObject = function(object, callback) { + var objects = object.objects, + options = fabric.util.object.clone(object, true); + delete options.objects; + if (typeof objects === 'string') { + // it has to be an url or something went wrong. + fabric.loadSVGFromURL(objects, function (elements) { + var group = fabric.util.groupSVGElements(elements, object, objects); + group.set(options); + callback && callback(group); + }); + return; + } + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + fabric.util.enlivenObjects([object.clipPath], function(enlivedClipPath) { + var options = fabric.util.object.clone(object, true); + options.clipPath = enlivedClipPath[0]; + delete options.objects; + callback && callback(new fabric.Group(enlivenedObjects, options, true)); + }); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.ActiveSelection) { + return; + } + + /** + * Group class + * @class fabric.ActiveSelection + * @extends fabric.Group + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.ActiveSelection#initialize} for constructor definition + */ + fabric.ActiveSelection = fabric.util.createClass(fabric.Group, /** @lends fabric.ActiveSelection.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'activeSelection', + + /** + * Constructor + * @param {Object} objects ActiveSelection objects + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(objects, options) { + options = options || {}; + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (options.originX) { + this.originX = options.originX; + } + if (options.originY) { + this.originY = options.originY; + } + this._calcBounds(); + this._updateObjectsCoords(); + fabric.Object.prototype.initialize.call(this, options); + this.setCoords(); + }, + + /** + * Change te activeSelection to a normal group, + * High level function that automatically adds it to canvas as + * active object. no events fired. + * @since 2.0.0 + * @return {fabric.Group} + */ + toGroup: function() { + var objects = this._objects.concat(); + this._objects = []; + var options = fabric.Object.prototype.toObject.call(this); + var newGroup = new fabric.Group([]); + delete options.type; + newGroup.set(options); + objects.forEach(function(object) { + object.canvas.remove(object); + object.group = newGroup; + }); + newGroup._objects = objects; + if (!this.canvas) { + return newGroup; + } + var canvas = this.canvas; + canvas.add(newGroup); + canvas._activeObject = newGroup; + newGroup.setCoords(); + return newGroup; + }, + + /** + * If returns true, deselection is cancelled. + * @since 2.0.0 + * @return {Boolean} [cancel] + */ + onDeselect: function() { + this.destroy(); + return false; + }, + + /** + * Returns string representation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return false; + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + * @param {Object} [childrenOverride] properties to override the children overrides + */ + _renderControls: function(ctx, styleOverride, childrenOverride) { + ctx.save(); + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + this.callSuper('_renderControls', ctx, styleOverride); + childrenOverride = childrenOverride || { }; + if (typeof childrenOverride.hasControls === 'undefined') { + childrenOverride.hasControls = false; + } + childrenOverride.forActiveSelection = true; + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx, childrenOverride); + } + ctx.restore(); + }, + }); + + /** + * Returns {@link fabric.ActiveSelection} instance from an object representation + * @static + * @memberOf fabric.ActiveSelection + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + */ + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + /** + * Image class + * @class fabric.Image + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} + * @see {@link fabric.Image#initialize} for constructor definition + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'image', + + /** + * Width of a stroke. + * For image quality a stroke multiple of 2 gives better results. + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * When calling {@link fabric.Image.getSrc}, return value from element src with `element.getAttribute('src')`. + * This allows for relative urls as image src. + * @since 2.7.0 + * @type Boolean + * @default + */ + srcFromAttribute: false, + + /** + * private + * contains last value of scaleX to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleX: 1, + + /** + * private + * contains last value of scaleY to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleY: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingX: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingY: 1, + + /** + * minimum scale factor under which any resizeFilter is triggered to resize the image + * 0 will disable the automatic resize. 1 will trigger automatically always. + * number bigger than 1 are not implemented yet. + * @type Number + */ + minimumScaleTrigger: 0.5, + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat('cropX', 'cropY'), + + /** + * key used to retrieve the texture representing this image + * @since 2.0.0 + * @type String + * @default + */ + cacheKey: '', + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropX: 0, + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropY: 0, + + /** + * Indicates whether this canvas will use image smoothing when painting this image. + * Also influence if the cacheCanvas for this image uses imageSmoothing + * @since 4.0.0-beta.11 + * @type Boolean + * @default + */ + imageSmoothing: true, + + /** + * Constructor + * Image can be initialized with any canvas drawable or a string. + * The string should be a url and will be loaded as an image. + * Canvas and Image element work out of the box, while videos require extra code to work. + * Please check video element events for seeking. + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element + * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. + * @return {fabric.Image} thisArg + */ + initialize: function(element, options) { + options || (options = { }); + this.filters = []; + this.cacheKey = 'texture' + fabric.Object.__uid++; + this.callSuper('initialize', options); + this._initElement(element, options); + }, + + /** + * Returns image element which this instance if based on + * @return {HTMLImageElement} Image element + */ + getElement: function() { + return this._element || {}; + }, + + /** + * Sets image element for this instance to a specified one. + * If filters defined they are applied to new image. + * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * @param {HTMLImageElement} element + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element, options) { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._element = element; + this._originalElement = element; + this._initConfig(options); + if (this.filters.length !== 0) { + this.applyFilters(); + } + // resizeFilters work on the already filtered copy. + // we need to apply resizeFilters AFTER normal filters. + // applyResizeFilters is run more often than normal filters + // and is triggered by user interactions rather than dev code + if (this.resizeFilter) { + this.applyResizeFilters(); + } + return this; + }, + + /** + * Delete a single texture if in webgl mode + */ + removeTexture: function(key) { + var backend = fabric.filterBackend; + if (backend && backend.evictCachesForKey) { + backend.evictCachesForKey(key); + } + }, + + /** + * Delete textures, reference to elements and eventually JSDOM cleanup + */ + dispose: function() { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._cacheContext = undefined; + ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + }, + + /** + * Get the crossOrigin value (of the corresponding image element) + */ + getCrossOrigin: function() { + return this._originalElement && (this._originalElement.crossOrigin || null); + }, + + /** + * Returns original size of an image + * @return {Object} Object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.naturalWidth || element.width, + height: element.naturalHeight || element.height + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + var w = this.width / 2, h = this.height / 2; + ctx.beginPath(); + ctx.moveTo(-w, -h); + ctx.lineTo(w, -h); + ctx.lineTo(w, h); + ctx.lineTo(-w, h); + ctx.lineTo(-w, -h); + ctx.closePath(); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var filters = []; + + this.filters.forEach(function(filterObj) { + if (filterObj) { + filters.push(filterObj.toObject()); + } + }); + var object = extend( + this.callSuper( + 'toObject', + ['cropX', 'cropY'].concat(propertiesToInclude) + ), { + src: this.getSrc(), + crossOrigin: this.getCrossOrigin(), + filters: filters, + }); + if (this.resizeFilter) { + object.resizeFilter = this.resizeFilter.toObject(); + } + return object; + }, + + /** + * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. + * @return {Boolean} + */ + hasCrop: function() { + return this.cropX || this.cropY || this.width < this._element.width || this.height < this._element.height; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString = [], imageMarkup = [], strokeSvg, element = this._element, + x = -this.width / 2, y = -this.height / 2, clipPath = '', imageRendering = ''; + if (!element) { + return []; + } + if (this.hasCrop()) { + var clipPathId = fabric.Object.__uid++; + svgString.push( + '\n', + '\t\n', + '\n' + ); + clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; + } + if (!this.imageSmoothing) { + imageRendering = '" image-rendering="optimizeSpeed'; + } + imageMarkup.push('\t\n'); + + if (this.stroke || this.strokeDashArray) { + var origFill = this.fill; + this.fill = null; + strokeSvg = [ + '\t\n' + ]; + this.fill = origFill; + } + if (this.paintFirst !== 'fill') { + svgString = svgString.concat(strokeSvg, imageMarkup); + } + else { + svgString = svgString.concat(imageMarkup, strokeSvg); + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * Returns source of an image + * @param {Boolean} filtered indicates if the src is needed for svg + * @return {String} Source of an image + */ + getSrc: function(filtered) { + var element = filtered ? this._element : this._originalElement; + if (element) { + if (element.toDataURL) { + return element.toDataURL(); + } + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } + else { + return element.src; + } + } + else { + return this.src || ''; + } + }, + + /** + * Sets source of an image + * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) + * @param {Object} [options] Options object + * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @return {fabric.Image} thisArg + * @chainable + */ + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img, isError) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this, isError); + }, this, options && options.crossOrigin); + return this; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + applyResizeFilters: function() { + var filter = this.resizeFilter, + minimumScale = this.minimumScaleTrigger, + objectScale = this.getTotalObjectScaling(), + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, + elementToFilter = this._filteredEl || this._originalElement; + if (this.group) { + this.set('dirty', true); + } + if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { + this._element = elementToFilter; + this._filterScalingX = 1; + this._filterScalingY = 1; + this._lastScaleX = scaleX; + this._lastScaleY = scaleY; + return; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + var canvasEl = fabric.util.createCanvasElement(), + cacheKey = this._filteredEl ? (this.cacheKey + '_filtered') : this.cacheKey, + sourceWidth = elementToFilter.width, sourceHeight = elementToFilter.height; + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._lastScaleX = filter.scaleX = scaleX; + this._lastScaleY = filter.scaleY = scaleY; + fabric.filterBackend.applyFilters( + [filter], elementToFilter, sourceWidth, sourceHeight, this._element, cacheKey); + this._filterScalingX = canvasEl.width / this._originalElement.width; + this._filterScalingY = canvasEl.height / this._originalElement.height; + }, + + /** + * Applies filters assigned to this image (from "filters" array) or from filter param + * @method applyFilters + * @param {Array} filters to be applied + * @param {Boolean} forResizing specify if the filter operation is a resize operation + * @return {thisArg} return the fabric.Image object + * @chainable + */ + applyFilters: function(filters) { + + filters = filters || this.filters || []; + filters = filters.filter(function(filter) { return filter && !filter.isNeutralState(); }); + this.set('dirty', true); + + // needs to clear out or WEBGL will not resize correctly + this.removeTexture(this.cacheKey + '_filtered'); + + if (filters.length === 0) { + this._element = this._originalElement; + this._filteredEl = null; + this._filterScalingX = 1; + this._filterScalingY = 1; + return this; + } + + var imgElement = this._originalElement, + sourceWidth = imgElement.naturalWidth || imgElement.width, + sourceHeight = imgElement.naturalHeight || imgElement.height; + + if (this._element === this._originalElement) { + // if the element is the same we need to create a new element + var canvasEl = fabric.util.createCanvasElement(); + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._filteredEl = canvasEl; + } + else { + // clear the existing element to get new filter data + // also dereference the eventual resized _element + this._element = this._filteredEl; + this._filteredEl.getContext('2d').clearRect(0, 0, sourceWidth, sourceHeight); + // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y + this._lastScaleX = 1; + this._lastScaleY = 1; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + fabric.filterBackend.applyFilters( + filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey); + if (this._originalElement.width !== this._element.width || + this._originalElement.height !== this._element.height) { + this._filterScalingX = this._element.width / this._originalElement.width; + this._filterScalingY = this._element.height / this._originalElement.height; + } + return this; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { + this.applyResizeFilters(); + } + this._stroke(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * it will set the imageSmoothing for the draw operation + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + fabric.Object.prototype.drawCacheOnCanvas.call(this, ctx); + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * This is the special image version where we would like to avoid caching where possible. + * Essentially images do not benefit from caching. They may require caching, and in that + * case we do it. Also caching an image usually ends in a loss of details. + * A full performance audit should be done. + * @return {Boolean} + */ + shouldCache: function() { + return this.needsItsOwnCache(); + }, + + _renderFill: function(ctx) { + var elementToDraw = this._element; + if (!elementToDraw) { + return; + } + var scaleX = this._filterScalingX, scaleY = this._filterScalingY, + w = this.width, h = this.height, min = Math.min, max = Math.max, + // crop values cannot be lesser than 0. + cropX = max(this.cropX, 0), cropY = max(this.cropY, 0), + elWidth = elementToDraw.naturalWidth || elementToDraw.width, + elHeight = elementToDraw.naturalHeight || elementToDraw.height, + sX = cropX * scaleX, + sY = cropY * scaleY, + // the width height cannot exceed element width/height, starting from the crop offset. + sW = min(w * scaleX, elWidth - sX), + sH = min(h * scaleY, elHeight - sY), + x = -w / 2, y = -h / 2, + maxDestW = min(w, elWidth / scaleX - cropX), + maxDestH = min(h, elHeight / scaleY - cropY); + + elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH); + }, + + /** + * needed to check if image needs resize + * @private + */ + _needsResize: function() { + var scale = this.getTotalObjectScaling(); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); + }, + + /** + * @private + */ + _resetWidthHeight: function() { + this.set(this.getOriginalSize()); + }, + + /** + * The Image class's initialization method. This method is automatically + * called by the constructor. + * @private + * @param {HTMLImageElement|String} element The element representing the image + * @param {Object} [options] Options object + */ + _initElement: function(element, options) { + this.setElement(fabric.util.getById(element), options); + fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initConfig: function(options) { + options || (options = { }); + this.setOptions(options); + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + + /** + * @private + * Set the width and the height of the image object, using the element or the + * options. + * @param {Object} [options] Object with width/height properties + */ + _setWidthHeight: function(options) { + options || (options = { }); + var el = this.getElement(); + this.width = options.width || el.naturalWidth || el.width || 0; + this.height = options.height || el.naturalHeight || el.height || 0; + }, + + /** + * Calculate offset for center and scale factor for the image in order to respect + * the preserveAspectRatio attribute + * @private + * @return {Object} + */ + parsePreserveAspectRatioAttribute: function() { + var pAR = fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio || ''), + rWidth = this._element.width, rHeight = this._element.height, + scaleX = 1, scaleY = 1, offsetLeft = 0, offsetTop = 0, cropX = 0, cropY = 0, + offset, pWidth = this.width, pHeight = this.height, parsedAttributes = { width: pWidth, height: pHeight }; + if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { + if (pAR.meetOrSlice === 'meet') { + scaleX = scaleY = fabric.util.findScaleToFit(this._element, parsedAttributes); + offset = (pWidth - rWidth * scaleX) / 2; + if (pAR.alignX === 'Min') { + offsetLeft = -offset; + } + if (pAR.alignX === 'Max') { + offsetLeft = offset; + } + offset = (pHeight - rHeight * scaleY) / 2; + if (pAR.alignY === 'Min') { + offsetTop = -offset; + } + if (pAR.alignY === 'Max') { + offsetTop = offset; + } + } + if (pAR.meetOrSlice === 'slice') { + scaleX = scaleY = fabric.util.findScaleToCover(this._element, parsedAttributes); + offset = rWidth - pWidth / scaleX; + if (pAR.alignX === 'Mid') { + cropX = offset / 2; + } + if (pAR.alignX === 'Max') { + cropX = offset; + } + offset = rHeight - pHeight / scaleY; + if (pAR.alignY === 'Mid') { + cropY = offset / 2; + } + if (pAR.alignY === 'Max') { + cropY = offset; + } + rWidth = pWidth / scaleX; + rHeight = pHeight / scaleY; + } + } + else { + scaleX = pWidth / rWidth; + scaleY = pHeight / rHeight; + } + return { + width: rWidth, + height: rHeight, + scaleX: scaleX, + scaleY: scaleY, + offsetLeft: offsetLeft, + offsetTop: offsetTop, + cropX: cropX, + cropY: cropY + }; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + * @default + */ + fabric.Image.CSS_CANVAS = 'canvas-img'; + + /** + * Alias for getSrc + * @static + */ + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} callback Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, isError) { + if (isError) { + callback && callback(null, true); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var image = new fabric.Image(img, object); + callback(image, false); + }); + }); + }); + }, null, object.crossOrigin); + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + fabric.util.loadImage(url, function(img, isError) { + callback && callback(new fabric.Image(img, imgOptions), isError); + }, null, imgOptions && imgOptions.crossOrigin); + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} + */ + fabric.Image.ATTRIBUTE_NAMES = + fabric.SHARED_ATTRIBUTES.concat( + 'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split(' ') + ); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} callback Callback to execute when fabric.Image object is created + * @return {fabric.Image} Instance of fabric.Image + */ + fabric.Image.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * @private + * @return {Number} angle value + */ + _getAngleValueForStraighten: function() { + var angle = this.angle % 360; + if (angle > 0) { + return Math.round((angle - 1) / 90) * 90; + } + return Math.round(angle / 90) * 90; + }, + + /** + * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) + * @return {fabric.Object} thisArg + * @chainable + */ + straighten: function() { + this.rotate(this._getAngleValueForStraighten()); + return this; + }, + + /** + * Same as {@link fabric.Object.prototype.straighten} but with animation + * @param {Object} callbacks Object with callback functions + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Object} thisArg + * @chainable + */ + fxStraighten: function(callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: this.get('angle'), + endValue: this._getAngleValueForStraighten(), + duration: this.FX_DURATION, + onChange: function(value) { + _this.rotate(value); + onChange(); + }, + onComplete: function() { + _this.setCoords(); + onComplete(); + }, + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Straightens object, then rerenders canvas + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.requestRenderAll(); + return this; + }, + + /** + * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.requestRenderAllBound + }); + return this; + } +}); + + +(function() { + + 'use strict'; + + /** + * Tests if webgl supports certain precision + * @param {WebGL} Canvas WebGL context to test on + * @param {String} Precision to test can be any of following: 'lowp', 'mediump', 'highp' + * @returns {Boolean} Whether the user's browser WebGL supports given precision. + */ + function testPrecision(gl, precision){ + var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}'; + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + return false; + } + return true; + } + + /** + * Indicate whether this filtering backend is supported by the user's browser. + * @param {Number} tileSize check if the tileSize is supported + * @returns {Boolean} Whether the user's browser supports WebGL. + */ + fabric.isWebglSupported = function(tileSize) { + if (fabric.isLikelyNode) { + return false; + } + tileSize = tileSize || fabric.WebglFilterBackend.prototype.tileSize; + var canvas = document.createElement('canvas'); + var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + var isSupported = false; + // eslint-disable-next-line + if (gl) { + fabric.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + isSupported = fabric.maxTextureSize >= tileSize; + var precisions = ['highp', 'mediump', 'lowp']; + for (var i = 0; i < 3; i++){ + if (testPrecision(gl, precisions[i])){ + fabric.webGlPrecision = precisions[i]; + break; + }; + } + } + this.isSupported = isSupported; + return isSupported; + }; + + fabric.WebglFilterBackend = WebglFilterBackend; + + /** + * WebGL filter backend. + */ + function WebglFilterBackend(options) { + if (options && options.tileSize) { + this.tileSize = options.tileSize; + } + this.setupGLContext(this.tileSize, this.tileSize); + this.captureGPUInfo(); + }; + + WebglFilterBackend.prototype = /** @lends fabric.WebglFilterBackend.prototype */ { + + tileSize: 2048, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Setup a WebGL context suitable for filtering, and bind any needed event handlers. + */ + setupGLContext: function(width, height) { + this.dispose(); + this.createWebGLCanvas(width, height); + // eslint-disable-next-line + this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); + this.chooseFastestCopyGLTo2DMethod(width, height); + }, + + /** + * Pick a method to copy data from GL context to 2d canvas. In some browsers using + * putImageData is faster than drawImage for that specific operation. + */ + chooseFastestCopyGLTo2DMethod: function(width, height) { + var canMeasurePerf = typeof window.performance !== 'undefined', canUseImageData; + try { + new ImageData(1, 1); + canUseImageData = true; + } + catch (e) { + canUseImageData = false; + } + // eslint-disable-next-line no-undef + var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; + // eslint-disable-next-line no-undef + var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; + + if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) { + return; + } + + var targetCanvas = fabric.util.createCanvasElement(); + // eslint-disable-next-line no-undef + var imageBuffer = new ArrayBuffer(width * height * 4); + if (fabric.forceGLPutImageData) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + return; + } + var testContext = { + imageBuffer: imageBuffer, + destinationWidth: width, + destinationHeight: height, + targetCanvas: targetCanvas + }; + var startTime, drawImageTime, putImageDataTime; + targetCanvas.width = width; + targetCanvas.height = height; + + startTime = window.performance.now(); + copyGLTo2DDrawImage.call(testContext, this.gl, testContext); + drawImageTime = window.performance.now() - startTime; + + startTime = window.performance.now(); + copyGLTo2DPutImageData.call(testContext, this.gl, testContext); + putImageDataTime = window.performance.now() - startTime; + + if (drawImageTime > putImageDataTime) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + } + else { + this.copyGLTo2D = copyGLTo2DDrawImage; + } + }, + + /** + * Create a canvas element and associated WebGL context and attaches them as + * class properties to the GLFilterBackend class. + */ + createWebGLCanvas: function(width, height) { + var canvas = fabric.util.createCanvasElement(); + canvas.width = width; + canvas.height = height; + var glOptions = { + alpha: true, + premultipliedAlpha: false, + depth: false, + stencil: false, + antialias: false + }, + gl = canvas.getContext('webgl', glOptions); + if (!gl) { + gl = canvas.getContext('experimental-webgl', glOptions); + } + if (!gl) { + return; + } + gl.clearColor(0, 0, 0, 0); + // this canvas can fire webglcontextlost and webglcontextrestored + this.canvas = canvas; + this.gl = gl; + }, + + /** + * Attempts to apply the requested filters to the source provided, drawing the filtered output + * to the provided target canvas. + * + * @param {Array} filters The filters to apply. + * @param {HTMLImageElement|HTMLCanvasElement} source The source to be filtered. + * @param {Number} width The width of the source input. + * @param {Number} height The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + * @param {String|undefined} cacheKey A key used to cache resources related to the source. If + * omitted, caching will be skipped. + */ + applyFilters: function(filters, source, width, height, targetCanvas, cacheKey) { + var gl = this.gl; + var cachedTexture; + if (cacheKey) { + cachedTexture = this.getCachedTexture(cacheKey, source); + } + var pipelineState = { + originalWidth: source.width || source.originalWidth, + originalHeight: source.height || source.originalHeight, + sourceWidth: width, + sourceHeight: height, + destinationWidth: width, + destinationHeight: height, + context: gl, + sourceTexture: this.createTexture(gl, width, height, !cachedTexture && source), + targetTexture: this.createTexture(gl, width, height), + originalTexture: cachedTexture || + this.createTexture(gl, width, height, !cachedTexture && source), + passes: filters.length, + webgl: true, + aPosition: this.aPosition, + programCache: this.programCache, + pass: 0, + filterBackend: this, + targetCanvas: targetCanvas + }; + var tempFbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); + filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); }); + resizeCanvasIfNeeded(pipelineState); + this.copyGLTo2D(gl, pipelineState); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.deleteTexture(pipelineState.sourceTexture); + gl.deleteTexture(pipelineState.targetTexture); + gl.deleteFramebuffer(tempFbo); + targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); + return pipelineState; + }, + + /** + * Detach event listeners, remove references, and clean up caches. + */ + dispose: function() { + if (this.canvas) { + this.canvas = null; + this.gl = null; + } + this.clearWebGLCaches(); + }, + + /** + * Wipe out WebGL-related caches. + */ + clearWebGLCaches: function() { + this.programCache = {}; + this.textureCache = {}; + }, + + /** + * Create a WebGL texture object. + * + * Accepts specific dimensions to initialize the texture to or a source image. + * + * @param {WebGLRenderingContext} gl The GL context to use for creating the texture. + * @param {Number} width The width to initialize the texture at. + * @param {Number} height The height to initialize the texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. + * @returns {WebGLTexture} + */ + createTexture: function(gl, width, height, textureImageSource) { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (textureImageSource) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImageSource); + } + else { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + return texture; + }, + + /** + * Can be optionally used to get a texture from the cache array + * + * If an existing texture is not found, a new texture is created and cached. + * + * @param {String} uniqueId A cache key to use to find an existing texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source to use to create the + * texture cache entry if one does not already exist. + */ + getCachedTexture: function(uniqueId, textureImageSource) { + if (this.textureCache[uniqueId]) { + return this.textureCache[uniqueId]; + } + else { + var texture = this.createTexture( + this.gl, textureImageSource.width, textureImageSource.height, textureImageSource); + this.textureCache[uniqueId] = texture; + return texture; + } + }, + + /** + * Clear out cached resources related to a source image that has been + * filtered previously. + * + * @param {String} cacheKey The cache key provided when the source image was filtered. + */ + evictCachesForKey: function(cacheKey) { + if (this.textureCache[cacheKey]) { + this.gl.deleteTexture(this.textureCache[cacheKey]); + delete this.textureCache[cacheKey]; + } + }, + + copyGLTo2D: copyGLTo2DDrawImage, + + /** + * Attempt to extract GPU information strings from a WebGL context. + * + * Useful information when debugging or blacklisting specific GPUs. + * + * @returns {Object} A GPU info object with renderer and vendor strings. + */ + captureGPUInfo: function() { + if (this.gpuInfo) { + return this.gpuInfo; + } + var gl = this.gl, gpuInfo = { renderer: '', vendor: '' }; + if (!gl) { + return gpuInfo; + } + var ext = gl.getExtension('WEBGL_debug_renderer_info'); + if (ext) { + var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); + var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); + if (renderer) { + gpuInfo.renderer = renderer.toLowerCase(); + } + if (vendor) { + gpuInfo.vendor = vendor.toLowerCase(); + } + } + this.gpuInfo = gpuInfo; + return gpuInfo; + }, + }; +})(); + +function resizeCanvasIfNeeded(pipelineState) { + var targetCanvas = pipelineState.targetCanvas, + width = targetCanvas.width, height = targetCanvas.height, + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight; + + if (width !== dWidth || height !== dHeight) { + targetCanvas.width = dWidth; + targetCanvas.height = dHeight; + } +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas. + * + * The WebGL canvas is assumed to be upside down, with the top-left pixel of the + * desired output image appearing in the bottom-left corner of the WebGL canvas. + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DDrawImage(gl, pipelineState) { + var glCanvas = gl.canvas, targetCanvas = pipelineState.targetCanvas, + ctx = targetCanvas.getContext('2d'); + ctx.translate(0, targetCanvas.height); // move it down again + ctx.scale(1, -1); // vertical flip + // where is my image on the big glcanvas? + var sourceY = glCanvas.height - targetCanvas.height; + ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, + targetCanvas.width, targetCanvas.height); +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData + * API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra). + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DPutImageData(gl, pipelineState) { + var targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext('2d'), + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight, + numBytes = dWidth * dHeight * 4; + + // eslint-disable-next-line no-undef + var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); + // eslint-disable-next-line no-undef + var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); + + gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8); + var imgData = new ImageData(u8Clamped, dWidth, dHeight); + ctx.putImageData(imgData, 0, 0); +} + + +(function() { + + 'use strict'; + + var noop = function() {}; + + fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; + + /** + * Canvas 2D filter backend. + */ + function Canvas2dFilterBackend() {}; + + Canvas2dFilterBackend.prototype = /** @lends fabric.Canvas2dFilterBackend.prototype */ { + evictCachesForKey: noop, + dispose: noop, + clearWebGLCaches: noop, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Apply a set of filters against a source image and draw the filtered output + * to the provided destination canvas. + * + * @param {EnhancedFilter} filters The filter to apply. + * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. + * @param {Number} sourceWidth The width of the source input. + * @param {Number} sourceHeight The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + */ + applyFilters: function(filters, sourceElement, sourceWidth, sourceHeight, targetCanvas) { + var ctx = targetCanvas.getContext('2d'); + ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); + var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var pipelineState = { + sourceWidth: sourceWidth, + sourceHeight: sourceHeight, + imageData: imageData, + originalEl: sourceElement, + originalImageData: originalImageData, + canvasEl: targetCanvas, + ctx: ctx, + filterBackend: this, + }; + filters.forEach(function(filter) { filter.applyTo(pipelineState); }); + if (pipelineState.imageData.width !== sourceWidth || pipelineState.imageData.height !== sourceHeight) { + targetCanvas.width = pipelineState.imageData.width; + targetCanvas.height = pipelineState.imageData.height; + } + ctx.putImageData(pipelineState.imageData, 0, 0); + return pipelineState; + }, + + }; +})(); + + +/** + * @namespace fabric.Image.filters + * @memberOf fabric.Image + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + */ +fabric.Image = fabric.Image || { }; +fabric.Image.filters = fabric.Image.filters || { }; + +/** + * Root filter class from which all filter classes inherit from + * @class fabric.Image.filters.BaseFilter + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'BaseFilter', + + /** + * Array of attributes to send with buffers. do not modify + * @private + */ + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + fragmentSource: 'precision highp float;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform sampler2D uTexture;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + '}', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Sets filter's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Compile this filter's shader program. + * + * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. + * @param {String} fragmentSource fragmentShader source for compilation + * @param {String} vertexSource vertexShader source for compilation + */ + createProgram: function(gl, fragmentSource, vertexSource) { + fragmentSource = fragmentSource || this.fragmentSource; + vertexSource = vertexSource || this.vertexSource; + if (fabric.webGlPrecision !== 'highp'){ + fragmentSource = fragmentSource.replace( + /precision highp float/g, + 'precision ' + fabric.webGlPrecision + ' float' + ); + } + var vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Vertex shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(vertexShader) + ); + } + + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Fragment shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(fragmentShader) + ); + } + + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Shader link error for "${this.type}" ' + + gl.getProgramInfoLog(program) + ); + } + + var attributeLocations = this.getAttributeLocations(gl, program); + var uniformLocations = this.getUniformLocations(gl, program) || { }; + uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); + uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); + return { + program: program, + attributeLocations: attributeLocations, + uniformLocations: uniformLocations + }; + }, + + /** + * Return a map of attribute names to WebGLAttributeLocation objects. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. + * @returns {Object} A map of attribute names to attribute locations. + */ + getAttributeLocations: function(gl, program) { + return { + aPosition: gl.getAttribLocation(program, 'aPosition'), + }; + }, + + /** + * Return a map of uniform names to WebGLUniformLocation objects. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. + * @returns {Object} A map of uniform names to uniform locations. + */ + getUniformLocations: function (/* gl, program */) { + // in case i do not need any special uniform i need to return an empty object + return { }; + }, + + /** + * Send attribute data from this filter to its shader program on the GPU. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} attributeLocations A map of shader attribute names to their locations. + */ + sendAttributeData: function(gl, attributeLocations, aPositionData) { + var attributeLocation = attributeLocations.aPosition; + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(attributeLocation); + gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); + }, + + _setupFrameBuffer: function(options) { + var gl = options.context, width, height; + if (options.passes > 1) { + width = options.destinationWidth; + height = options.destinationHeight; + if (options.sourceWidth !== width || options.sourceHeight !== height) { + gl.deleteTexture(options.targetTexture); + options.targetTexture = options.filterBackend.createTexture(gl, width, height); + } + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, + options.targetTexture, 0); + } + else { + // draw last filter on canvas and not to framebuffer. + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.finish(); + } + }, + + _swapTextures: function(options) { + options.passes--; + options.pass++; + var temp = options.targetTexture; + options.targetTexture = options.sourceTexture; + options.sourceTexture = temp; + }, + + /** + * Generic isNeutral implementation for one parameter based filters. + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) + * @param {Object} options + **/ + isNeutralState: function(/* options */) { + var main = this.mainParameter, + _class = fabric.Image.filters[this.type].prototype; + if (main) { + if (Array.isArray(_class[main])) { + for (var i = _class[main].length; i--;) { + if (this[main][i] !== _class[main][i]) { + return false; + } + } + return true; + } + else { + return _class[main] === this[main]; + } + } + else { + return false; + } + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + if (!options.programCache.hasOwnProperty(this.type)) { + options.programCache[this.type] = this.createProgram(options.context); + } + return options.programCache[this.type]; + }, + + /** + * Apply this filter using webgl. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.originalTexture The texture of the original input image. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyToWebGL: function(options) { + var gl = options.context; + var shader = this.retrieveShader(options); + if (options.pass === 0 && options.originalTexture) { + gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); + } + else { + gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); + } + gl.useProgram(shader.program); + this.sendAttributeData(gl, shader.attributeLocations, options.aPosition); + + gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); + gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); + + this.sendUniformData(gl, shader.uniformLocations); + gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + + bindAdditionalTexture: function(gl, texture, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, texture); + // reset active texture to 0 as usual + gl.activeTexture(gl.TEXTURE0); + }, + + unbindAdditionalTexture: function(gl, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.activeTexture(gl.TEXTURE0); + }, + + getMainParameter: function() { + return this[this.mainParameter]; + }, + + setMainParameter: function(value) { + this[this.mainParameter] = value; + }, + + /** + * Send uniform data from this filter to its shader program on the GPU. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} uniformLocations A map of shader uniform names to their locations. + */ + sendUniformData: function(/* gl, uniformLocations */) { + // Intentionally left blank. Override me in subclasses. + }, + + /** + * If needed by a 2d filter, this functions can create an helper canvas to be used + * remember that options.targetCanvas is available for use till end of chain. + */ + createHelpLayer: function(options) { + if (!options.helpLayer) { + var helpLayer = document.createElement('canvas'); + helpLayer.width = options.sourceWidth; + helpLayer.height = options.sourceHeight; + options.helpLayer = helpLayer; + } + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + var object = { type: this.type }, mainP = this.mainParameter; + if (mainP) { + object[mainP] = this[mainP]; + } + return object; + }, + + /** + * Returns a JSON representation of an instance + * @return {Object} JSON + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + } +}); + +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; +}; + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Matrix filter class + * @class fabric.Image.filters.ColorMatrix + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php} + * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl} + * @example Kodachrome filter + * var filter = new fabric.Image.filters.ColorMatrix({ + * matrix: [ + 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502, + -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203, + -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946, + 0, 0, 0, 1, 0 + ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'ColorMatrix', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform mat4 uColorMatrix;\n' + + 'uniform vec4 uConstants;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color *= uColorMatrix;\n' + + 'color += uConstants;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Colormatrix for pixels. + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ], + + mainParameter: 'matrix', + + /** + * Lock the colormatrix on the color part, skipping alpha, manly for non webgl scenario + * to save some calculation + */ + colorsOnly: true, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.matrix = this.matrix.slice(0); + }, + + /** + * Apply the ColorMatrix operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = data.length, + m = this.matrix, + r, g, b, a, i, colorsOnly = this.colorsOnly; + + for (i = 0; i < iLen; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + if (colorsOnly) { + data[i] = r * m[0] + g * m[1] + b * m[2] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + m[14] * 255; + } + else { + a = data[i + 3]; + data[i] = r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255; + data[i + 3] = r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'), + uConstants: gl.getUniformLocation(program, 'uConstants'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var m = this.matrix, + matrix = [ + m[0], m[1], m[2], m[3], + m[5], m[6], m[7], m[8], + m[10], m[11], m[12], m[13], + m[15], m[16], m[17], m[18] + ], + constants = [m[4], m[9], m[14], m[19]]; + gl.uniformMatrix4fv(uniformLocations.uColorMatrix, false, matrix); + gl.uniform4fv(uniformLocations.uConstants, constants); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + */ + fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Brightness filter class + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 0.05 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Brightness', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBrightness;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += uBrightness;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Brightness value, from -1 to 1. + * translated to -255 to 255 for 2d + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Number} brightness + * @default + */ + brightness: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'brightness', + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.brightness === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + brightness = Math.round(this.brightness * 255); + for (i = 0; i < len; i += 4) { + data[i] = data[i] + brightness; + data[i + 1] = data[i + 1] + brightness; + data[i + 2] = data[i + 2] + brightness; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBrightness: gl.getUniformLocation(program, 'uBrightness'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBrightness, this.brightness); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + */ + fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Adapted from html5rocks article + * @class fabric.Image.filters.Convolute + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Convolute', + + /* + * Opaque value (true/false) + */ + opaque: false, + + /* + * matrix for the filter, max 9x9 + */ + matrix: [0, 0, 0, 0, 1, 0, 0, 0, 0], + + /** + * Fragment source for the brightness program + */ + fragmentSource: { + Convolute_3_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_3_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_5_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_5_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_7_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_7_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_9_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_9_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + }, + + /** + * Constructor + * @memberOf fabric.Image.filters.Convolute.prototype + * @param {Object} [options] Options object + * @param {Boolean} [options.opaque=false] Opaque value (true/false) + * @param {Array} [options.matrix] Filter matrix + */ + + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var size = Math.sqrt(this.matrix.length); + var cacheKey = this.type + '_' + size + '_' + (this.opaque ? 1 : 0); + var shaderSource = this.fragmentSource[cacheKey]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + weights = this.matrix, + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side / 2), + sw = imageData.width, + sh = imageData.height, + output = options.ctx.createImageData(sw, sh), + dst = output.data, + // go through the destination image pixels + alphaFac = this.opaque ? 1 : 0, + r, g, b, a, dstOff, + scx, scy, srcOff, wt, + x, y, cx, cy; + + for (y = 0; y < sh; y++) { + for (x = 0; x < sw; x++) { + dstOff = (y * sw + x) * 4; + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + r = 0; g = 0; b = 0; a = 0; + + for (cy = 0; cy < side; cy++) { + for (cx = 0; cx < side; cx++) { + scy = y + cy - halfSide; + scx = x + cx - halfSide; + + // eslint-disable-next-line max-depth + if (scy < 0 || scy >= sh || scx < 0 || scx >= sw) { + continue; + } + + srcOff = (scy * sw + scx) * 4; + wt = weights[cy * side + cx]; + + r += data[srcOff] * wt; + g += data[srcOff + 1] * wt; + b += data[srcOff + 2] * wt; + // eslint-disable-next-line max-depth + if (!alphaFac) { + a += data[srcOff + 3] * wt; + } + } + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + if (!alphaFac) { + dst[dstOff + 3] = a; + } + else { + dst[dstOff + 3] = data[dstOff + 3]; + } + } + } + options.imageData = output; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMatrix: gl.getUniformLocation(program, 'uMatrix'), + uOpaque: gl.getUniformLocation(program, 'uOpaque'), + uHalfSize: gl.getUniformLocation(program, 'uHalfSize'), + uSize: gl.getUniformLocation(program, 'uSize'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1fv(uniformLocations.uMatrix, this.matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + opaque: this.opaque, + matrix: this.matrix + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + */ + fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Grayscale image filter class + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Grayscale', + + fragmentSource: { + average: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float average = (color.r + color.b + color.g) / 3.0;\n' + + 'gl_FragColor = vec4(average, average, average, color.a);\n' + + '}', + lightness: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + luminosity: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + }, + + + /** + * Grayscale mode, between 'average', 'lightness', 'luminosity' + * @param {String} type + * @default + */ + mode: 'average', + + mainParameter: 'mode', + + /** + * Apply the Grayscale operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length, value, + mode = this.mode; + for (i = 0; i < len; i += 4) { + if (mode === 'average') { + value = (data[i] + data[i + 1] + data[i + 2]) / 3; + } + else if (mode === 'lightness') { + value = (Math.min(data[i], data[i + 1], data[i + 2]) + + Math.max(data[i], data[i + 1], data[i + 2])) / 2; + } + else if (mode === 'luminosity') { + value = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2]; + } + data[i] = value; + data[i + 1] = value; + data[i + 2] = value; + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var shaderSource = this.fragmentSource[this.mode]; + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMode: gl.getUniformLocation(program, 'uMode'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + // default average mode. + var mode = 1; + gl.uniform1i(uniformLocations.uMode, mode); + }, + + /** + * Grayscale filter isNeutralState implementation + * The filter is never neutral + * on the image + **/ + isNeutralState: function() { + return false; + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + */ + fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Invert filter class + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Invert = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Invert', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uInvert;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'if (uInvert == 1) {\n' + + 'gl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n' + + '} else {\n' + + 'gl_FragColor = color;\n' + + '}\n' + + '}', + + /** + * Filter invert. if false, does nothing + * @param {Boolean} invert + * @default + */ + invert: true, + + mainParameter: 'invert', + + /** + * Apply the Invert operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length; + for (i = 0; i < len; i += 4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + }, + + /** + * Invert filter isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function() { + return !this.invert; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uInvert: gl.getUniformLocation(program, 'uInvert'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1i(uniformLocations.uInvert, this.invert); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + */ + fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; + + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Noise filter class + * @class fabric.Image.filters.Noise + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Noise', + + /** + * Fragment source for the noise program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uStepH;\n' + + 'uniform float uNoise;\n' + + 'uniform float uSeed;\n' + + 'varying vec2 vTexCoord;\n' + + 'float rand(vec2 co, float seed, float vScale) {\n' + + 'return fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'noise', + + /** + * Noise value, from + * @param {Number} noise + * @default + */ + noise: 0, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.noise === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + noise = this.noise, rand; + + for (i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uNoise: gl.getUniformLocation(program, 'uNoise'), + uSeed: gl.getUniformLocation(program, 'uSeed'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uNoise, this.noise / 255); + gl.uniform1f(uniformLocations.uSeed, Math.random()); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + noise: this.noise + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + */ + fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Pixelate filter class + * @class fabric.Image.filters.Pixelate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Pixelate', + + blocksize: 4, + + mainParameter: 'blocksize', + + /** + * Fragment source for the Pixelate program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBlocksize;\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'float blockW = uBlocksize * uStepW;\n' + + 'float blockH = uBlocksize * uStepW;\n' + + 'int posX = int(vTexCoord.x / blockW);\n' + + 'int posY = int(vTexCoord.y / blockH);\n' + + 'float fposX = float(posX);\n' + + 'float fposY = float(posY);\n' + + 'vec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\n' + + 'vec4 color = texture2D(uTexture, squareCoords);\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Apply the Pixelate operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = imageData.height, + jLen = imageData.width, + index, i, j, r, g, b, a, + _i, _j, _iLen, _jLen; + + for (i = 0; i < iLen; i += this.blocksize) { + for (j = 0; j < jLen; j += this.blocksize) { + + index = (i * 4) * jLen + (j * 4); + + r = data[index]; + g = data[index + 1]; + b = data[index + 2]; + a = data[index + 3]; + + _iLen = Math.min(i + this.blocksize, iLen); + _jLen = Math.min(j + this.blocksize, jLen); + for (_i = i; _i < _iLen; _i++) { + for (_j = j; _j < _jLen; _j++) { + index = (_i * 4) * jLen + (_j * 4); + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = a; + } + } + } + } + }, + + /** + * Indicate when the filter is not gonna apply changes to the image + **/ + isNeutralState: function() { + return this.blocksize === 1; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBlocksize: gl.getUniformLocation(program, 'uBlocksize'), + uStepW: gl.getUniformLocation(program, 'uStepW'), + uStepH: gl.getUniformLocation(program, 'uStepH'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBlocksize, this.blocksize); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + */ + fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Remove white filter class + * @class fabric.Image.filters.RemoveColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveColor#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveColor({ + * threshold: 0.2, + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'RemoveColor', + + /** + * Color to remove, in any format understood by fabric.Color. + * @param {String} type + * @default + */ + color: '#FFFFFF', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uLow;\n' + + 'uniform vec4 uHigh;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + 'if(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\n' + + 'gl_FragColor.a = 0.0;\n' + + '}\n' + + '}', + + /** + * distance to actual color, as value up or down from each r,g,b + * between 0 and 1 + **/ + distance: 0.02, + + /** + * For color to remove inside distance, use alpha channel for a smoother deletion + * NOT IMPLEMENTED YET + **/ + useAlpha: false, + + /** + * Constructor + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + * @param {Number} [options.color=#RRGGBB] Threshold value + * @param {Number} [options.distance=10] Distance value + */ + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + distance = this.distance * 255, + r, g, b, + source = new fabric.Color(this.color).getSource(), + lowC = [ + source[0] - distance, + source[1] - distance, + source[2] - distance, + ], + highC = [ + source[0] + distance, + source[1] + distance, + source[2] + distance, + ]; + + + for (i = 0; i < data.length; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + if (r > lowC[0] && + g > lowC[1] && + b > lowC[2] && + r < highC[0] && + g < highC[1] && + b < highC[2]) { + data[i + 3] = 0; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uLow: gl.getUniformLocation(program, 'uLow'), + uHigh: gl.getUniformLocation(program, 'uHigh'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(), + distance = parseFloat(this.distance), + lowC = [ + 0 + source[0] / 255 - distance, + 0 + source[1] / 255 - distance, + 0 + source[2] / 255 - distance, + 1 + ], + highC = [ + source[0] / 255 + distance, + source[1] / 255 + distance, + source[2] / 255 + distance, + 1 + ]; + gl.uniform4fv(uniformLocations.uLow, lowC); + gl.uniform4fv(uniformLocations.uHigh, highC); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + color: this.color, + distance: this.distance + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite + */ + fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + var matrices = { + Brownie: [ + 0.59970,0.34553,-0.27082,0,0.186, + -0.03770,0.86095,0.15059,0,-0.1449, + 0.24113,-0.07441,0.44972,0,-0.02965, + 0,0,0,1,0 + ], + Vintage: [ + 0.62793,0.32021,-0.03965,0,0.03784, + 0.02578,0.64411,0.03259,0,0.02926, + 0.04660,-0.08512,0.52416,0,0.02023, + 0,0,0,1,0 + ], + Kodachrome: [ + 1.12855,-0.39673,-0.03992,0,0.24991, + -0.16404,1.08352,-0.05498,0,0.09698, + -0.16786,-0.56034,1.60148,0,0.13972, + 0,0,0,1,0 + ], + Technicolor: [ + 1.91252,-0.85453,-0.09155,0,0.04624, + -0.30878,1.76589,-0.10601,0,-0.27589, + -0.23110,-0.75018,1.84759,0,0.12137, + 0,0,0,1,0 + ], + Polaroid: [ + 1.438,-0.062,-0.062,0,0, + -0.122,1.378,-0.122,0,0, + -0.016,-0.016,1.483,0,0, + 0,0,0,1,0 + ], + Sepia: [ + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0 + ], + BlackWhite: [ + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 0, 0, 0, 1, 0, + ] + }; + + for (var key in matrices) { + filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: key, + + /** + * Colormatrix for the effect + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: matrices[key], + + /** + * Lock the matrix export for this kind of static, parameter less filters. + */ + mainParameter: false, + /** + * Lock the colormatrix on the color part, skipping alpha + */ + colorsOnly: true, + + }); + fabric.Image.filters[key].fromObject = fabric.Image.filters.BaseFilter.fromObject; + } +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Blend filter class + * @class fabric.Image.filter.BlendColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { + type: 'BlendColor', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + color: '#F95C63', + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend color operation. + **/ + alpha: 1, + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'gl_FragColor.rgb *= uColor.rgb;\n', + screen: 'gl_FragColor.rgb = 1.0 - (1.0 - gl_FragColor.rgb) * (1.0 - uColor.rgb);\n', + add: 'gl_FragColor.rgb += uColor.rgb;\n', + diff: 'gl_FragColor.rgb = abs(gl_FragColor.rgb - uColor.rgb);\n', + subtract: 'gl_FragColor.rgb -= uColor.rgb;\n', + lighten: 'gl_FragColor.rgb = max(gl_FragColor.rgb, uColor.rgb);\n', + darken: 'gl_FragColor.rgb = min(gl_FragColor.rgb, uColor.rgb);\n', + exclusion: 'gl_FragColor.rgb += uColor.rgb - 2.0 * (uColor.rgb * gl_FragColor.rgb);\n', + overlay: 'if (uColor.r < 0.5) {\n' + + 'gl_FragColor.r *= 2.0 * uColor.r;\n' + + '} else {\n' + + 'gl_FragColor.r = 1.0 - 2.0 * (1.0 - gl_FragColor.r) * (1.0 - uColor.r);\n' + + '}\n' + + 'if (uColor.g < 0.5) {\n' + + 'gl_FragColor.g *= 2.0 * uColor.g;\n' + + '} else {\n' + + 'gl_FragColor.g = 1.0 - 2.0 * (1.0 - gl_FragColor.g) * (1.0 - uColor.g);\n' + + '}\n' + + 'if (uColor.b < 0.5) {\n' + + 'gl_FragColor.b *= 2.0 * uColor.b;\n' + + '} else {\n' + + 'gl_FragColor.b = 1.0 - 2.0 * (1.0 - gl_FragColor.b) * (1.0 - uColor.b);\n' + + '}\n', + tint: 'gl_FragColor.rgb *= (1.0 - uColor.a);\n' + + 'gl_FragColor.rgb += uColor.rgb;\n', + }, + + /** + * build the fragment source for the filters, joining the common part with + * the specific one. + * @param {String} mode the mode of the filter, a key of this.fragmentSource + * @return {String} the source to be compiled + * @private + */ + buildSource: function(mode) { + return 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'gl_FragColor = color;\n' + + 'if (color.a > 0.0) {\n' + + this.fragmentSource[mode] + + '}\n' + + '}'; + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode, shaderSource; + if (!options.programCache.hasOwnProperty(cacheKey)) { + shaderSource = this.buildSource(this.mode); + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, iLen = data.length, + tr, tg, tb, + r, g, b, + source, alpha1 = 1 - this.alpha; + + source = new fabric.Color(this.color).getSource(); + tr = source[0] * this.alpha; + tg = source[1] * this.alpha; + tb = source[2] * this.alpha; + + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + break; + case 'screen': + data[i] = 255 - (255 - r) * (255 - tr) / 255; + data[i + 1] = 255 - (255 - g) * (255 - tg) / 255; + data[i + 2] = 255 - (255 - b) * (255 - tb) / 255; + break; + case 'add': + data[i] = r + tr; + data[i + 1] = g + tg; + data[i + 2] = b + tb; + break; + case 'diff': + case 'difference': + data[i] = Math.abs(r - tr); + data[i + 1] = Math.abs(g - tg); + data[i + 2] = Math.abs(b - tb); + break; + case 'subtract': + data[i] = r - tr; + data[i + 1] = g - tg; + data[i + 2] = b - tb; + break; + case 'darken': + data[i] = Math.min(r, tr); + data[i + 1] = Math.min(g, tg); + data[i + 2] = Math.min(b, tb); + break; + case 'lighten': + data[i] = Math.max(r, tr); + data[i + 1] = Math.max(g, tg); + data[i + 2] = Math.max(b, tb); + break; + case 'overlay': + data[i] = tr < 128 ? (2 * r * tr / 255) : (255 - 2 * (255 - r) * (255 - tr) / 255); + data[i + 1] = tg < 128 ? (2 * g * tg / 255) : (255 - 2 * (255 - g) * (255 - tg) / 255); + data[i + 2] = tb < 128 ? (2 * b * tb / 255) : (255 - 2 * (255 - b) * (255 - tb) / 255); + break; + case 'exclusion': + data[i] = tr + r - ((2 * tr * r) / 255); + data[i + 1] = tg + g - ((2 * tg * g) / 255); + data[i + 2] = tb + b - ((2 * tb * b) / 255); + break; + case 'tint': + data[i] = tr + r * alpha1; + data[i + 1] = tg + g * alpha1; + data[i + 2] = tb + b * alpha1; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColor: gl.getUniformLocation(program, 'uColor'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(); + source[0] = this.alpha * source[0] / 255; + source[1] = this.alpha * source[1] / 255; + source[2] = this.alpha * source[2] / 255; + source[3] = this.alpha; + gl.uniform4fv(uniformLocations.uColor, source); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + color: this.color, + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor + */ + fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Image Blend filter class + * @class fabric.Image.filter.BlendImage + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendImage = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.BlendImage.prototype */ { + type: 'BlendImage', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + image: null, + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend image operation. + * not implemented. + **/ + alpha: 1, + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'uniform mat3 uTransformMatrix;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'vTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.rgba *= color2.rgba;\n' + + 'gl_FragColor = color;\n' + + '}', + mask: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.a = color2.a;\n' + + 'gl_FragColor = color;\n' + + '}', + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + var shaderSource = this.fragmentSource[this.mode]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + applyToWebGL: function(options) { + // load texture to blend. + var gl = options.context, + texture = this.createTexture(options.filterBackend, this.image); + this.bindAdditionalTexture(gl, texture, gl.TEXTURE1); + this.callSuper('applyToWebGL', options); + this.unbindAdditionalTexture(gl, gl.TEXTURE1); + }, + + createTexture: function(backend, image) { + return backend.getCachedTexture(image.cacheKey, image._element); + }, + + /** + * Calculate a transformMatrix to adapt the image to blend over + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + calculateMatrix: function() { + var image = this.image, + width = image._element.width, + height = image._element.height; + return [ + 1 / image.scaleX, 0, 0, + 0, 1 / image.scaleY, 0, + -image.left / width, -image.top / height, 1 + ]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + resources = options.filterBackend.resources, + data = imageData.data, iLen = data.length, + width = imageData.width, + height = imageData.height, + tr, tg, tb, ta, + r, g, b, a, + canvas1, context, image = this.image, blendData; + + if (!resources.blendImage) { + resources.blendImage = fabric.util.createCanvasElement(); + } + canvas1 = resources.blendImage; + context = canvas1.getContext('2d'); + if (canvas1.width !== width || canvas1.height !== height) { + canvas1.width = width; + canvas1.height = height; + } + else { + context.clearRect(0, 0, width, height); + } + context.setTransform(image.scaleX, 0, 0, image.scaleY, image.left, image.top); + context.drawImage(image._element, 0, 0, width, height); + blendData = context.getImageData(0, 0, width, height).data; + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + + tr = blendData[i]; + tg = blendData[i + 1]; + tb = blendData[i + 2]; + ta = blendData[i + 3]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + data[i + 3] = a * ta / 255; + break; + case 'mask': + data[i + 3] = ta; + break; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uTransformMatrix: gl.getUniformLocation(program, 'uTransformMatrix'), + uImage: gl.getUniformLocation(program, 'uImage'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var matrix = this.calculateMatrix(); + gl.uniform1i(uniformLocations.uImage, 1); // texture unit 1. + gl.uniformMatrix3fv(uniformLocations.uTransformMatrix, false, matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + image: this.image && this.image.toObject(), + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} callback to be invoked after filter creation + * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage + */ + fabric.Image.filters.BlendImage.fromObject = function(object, callback) { + fabric.Image.fromObject(object.image, function(image) { + var options = fabric.util.object.clone(object); + options.image = image; + callback(new fabric.Image.filters.BlendImage(options)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, + sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin, + ceil = Math.ceil, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Resize image filter class + * @class fabric.Image.filters.Resize + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Resize(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Resize', + + /** + * Resize type + * for webgl resizeType is just lanczos, for canvas2d can be: + * bilinear, hermite, sliceHack, lanczos. + * @param {String} resizeType + * @default + */ + resizeType: 'hermite', + + /** + * Scale factor for resizing, x axis + * @param {Number} scaleX + * @default + */ + scaleX: 1, + + /** + * Scale factor for resizing, y axis + * @param {Number} scaleY + * @default + */ + scaleY: 1, + + /** + * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos + * @param {Number} lanczosLobes + * @default + */ + lanczosLobes: 3, + + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uDelta: gl.getUniformLocation(program, 'uDelta'), + uTaps: gl.getUniformLocation(program, 'uTaps'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform2fv(uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height]); + gl.uniform1fv(uniformLocations.uTaps, this.taps); + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var filterWindow = this.getFilterWindow(), cacheKey = this.type + '_' + filterWindow; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var fragmentShader = this.generateShader(filterWindow); + options.programCache[cacheKey] = this.createProgram(options.context, fragmentShader); + } + return options.programCache[cacheKey]; + }, + + getFilterWindow: function() { + var scale = this.tempScale; + return Math.ceil(this.lanczosLobes / scale); + }, + + getTaps: function() { + var lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, + filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); + for (var i = 1; i <= filterWindow; i++) { + taps[i - 1] = lobeFunction(i * scale); + } + return taps; + }, + + /** + * Generate vertex and shader sources from the necessary steps numbers + * @param {Number} filterWindow + */ + generateShader: function(filterWindow) { + var offsets = new Array(filterWindow), + fragmentShader = this.fragmentSourceTOP, filterWindow; + + for (var i = 1; i <= filterWindow; i++) { + offsets[i - 1] = i + '.0 * uDelta'; + } + + fragmentShader += 'uniform float uTaps[' + filterWindow + '];\n'; + fragmentShader += 'void main() {\n'; + fragmentShader += ' vec4 color = texture2D(uTexture, vTexCoord);\n'; + fragmentShader += ' float sum = 1.0;\n'; + + offsets.forEach(function(offset, i) { + fragmentShader += ' color += texture2D(uTexture, vTexCoord + ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' color += texture2D(uTexture, vTexCoord - ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' sum += 2.0 * uTaps[' + i + '];\n'; + }); + fragmentShader += ' gl_FragColor = color / sum;\n'; + fragmentShader += '}'; + return fragmentShader; + }, + + fragmentSourceTOP: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n', + + /** + * Apply the resize filter to the image + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + options.passes++; + this.width = options.sourceWidth; + this.horizontal = true; + this.dW = Math.round(this.width * this.scaleX); + this.dH = options.sourceHeight; + this.tempScale = this.dW / this.width; + this.taps = this.getTaps(); + options.destinationWidth = this.dW; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceWidth = options.destinationWidth; + + this.height = options.sourceHeight; + this.horizontal = false; + this.dH = Math.round(this.height * this.scaleY); + this.tempScale = this.dH / this.height; + this.taps = this.getTaps(); + options.destinationHeight = this.dH; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceHeight = options.destinationHeight; + } + else { + this.applyTo2d(options); + } + }, + + isNeutralState: function() { + return this.scaleX === 1 && this.scaleY === 1; + }, + + lanczosCreate: function(lobes) { + return function(x) { + if (x >= lobes || x <= -lobes) { + return 0.0; + } + if (x < 1.19209290E-07 && x > -1.19209290E-07) { + return 1.0; + } + x *= Math.PI; + var xx = x / lobes; + return (sin(x) / x) * sin(xx) / xx; + }; + }, + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Resize.prototype + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} scaleX + * @param {Number} scaleY + */ + applyTo2d: function(options) { + var imageData = options.imageData, + scaleX = this.scaleX, + scaleY = this.scaleY; + + this.rcpScaleX = 1 / scaleX; + this.rcpScaleY = 1 / scaleY; + + var oW = imageData.width, oH = imageData.height, + dW = round(oW * scaleX), dH = round(oH * scaleY), + newData; + + if (this.resizeType === 'sliceHack') { + newData = this.sliceByTwo(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'hermite') { + newData = this.hermiteFastResize(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'bilinear') { + newData = this.bilinearFiltering(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'lanczos') { + newData = this.lanczosResize(options, oW, oH, dW, dH); + } + options.imageData = newData; + }, + + /** + * Filter sliceByTwo + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + sliceByTwo: function(options, oW, oH, dW, dH) { + var imageData = options.imageData, + mult = 0.5, doneW = false, doneH = false, stepW = oW * mult, + stepH = oH * mult, resources = fabric.filterBackend.resources, + tmpCanvas, ctx, sX = 0, sY = 0, dX = oW, dY = 0; + if (!resources.sliceByTwo) { + resources.sliceByTwo = document.createElement('canvas'); + } + tmpCanvas = resources.sliceByTwo; + if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) { + tmpCanvas.width = oW * 1.5; + tmpCanvas.height = oH; + } + ctx = tmpCanvas.getContext('2d'); + ctx.clearRect(0, 0, oW * 1.5, oH); + ctx.putImageData(imageData, 0, 0); + + dW = floor(dW); + dH = floor(dH); + + while (!doneW || !doneH) { + oW = stepW; + oH = stepH; + if (dW < floor(stepW * mult)) { + stepW = floor(stepW * mult); + } + else { + stepW = dW; + doneW = true; + } + if (dH < floor(stepH * mult)) { + stepH = floor(stepH * mult); + } + else { + stepH = dH; + doneH = true; + } + ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); + sX = dX; + sY = dY; + dY += stepH; + } + return ctx.getImageData(sX, sY, dW, dH); + }, + + /** + * Filter lanczosResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + lanczosResize: function(options, oW, oH, dW, dH) { + + function process(u) { + var v, i, weight, idx, a, red, green, + blue, alpha, fX, fY; + center.x = (u + 0.5) * ratioX; + icenter.x = floor(center.x); + for (v = 0; v < dH; v++) { + center.y = (v + 0.5) * ratioY; + icenter.y = floor(center.y); + a = 0; red = 0; green = 0; blue = 0; alpha = 0; + for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { + if (i < 0 || i >= oW) { + continue; + } + fX = floor(1000 * abs(i - center.x)); + if (!cacheLanc[fX]) { + cacheLanc[fX] = { }; + } + for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { + if (j < 0 || j >= oH) { + continue; + } + fY = floor(1000 * abs(j - center.y)); + if (!cacheLanc[fX][fY]) { + cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); + } + weight = cacheLanc[fX][fY]; + if (weight > 0) { + idx = (j * oW + i) * 4; + a += weight; + red += weight * srcData[idx]; + green += weight * srcData[idx + 1]; + blue += weight * srcData[idx + 2]; + alpha += weight * srcData[idx + 3]; + } + } + } + idx = (v * dW + u) * 4; + destData[idx] = red / a; + destData[idx + 1] = green / a; + destData[idx + 2] = blue / a; + destData[idx + 3] = alpha / a; + } + + if (++u < dW) { + return process(u); + } + else { + return destImg; + } + } + + var srcData = options.imageData.data, + destImg = options.ctx.createImageData(dW, dH), + destData = destImg.data, + lanczos = this.lanczosCreate(this.lanczosLobes), + ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, + rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, + range2X = ceil(ratioX * this.lanczosLobes / 2), + range2Y = ceil(ratioY * this.lanczosLobes / 2), + cacheLanc = { }, center = { }, icenter = { }; + + return process(0); + }, + + /** + * bilinearFiltering + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + bilinearFiltering: function(options, oW, oH, dW, dH) { + var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, + color, offset = 0, origPix, ratioX = this.rcpScaleX, + ratioY = this.rcpScaleY, + w4 = 4 * (oW - 1), img = options.imageData, + pixels = img.data, destImage = options.ctx.createImageData(dW, dH), + destPixels = destImage.data; + for (i = 0; i < dH; i++) { + for (j = 0; j < dW; j++) { + x = floor(ratioX * j); + y = floor(ratioY * i); + xDiff = ratioX * j - x; + yDiff = ratioY * i - y; + origPix = 4 * (y * oW + x); + + for (chnl = 0; chnl < 4; chnl++) { + a = pixels[origPix + chnl]; + b = pixels[origPix + 4 + chnl]; + c = pixels[origPix + w4 + chnl]; + d = pixels[origPix + w4 + 4 + chnl]; + color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; + destPixels[offset++] = color; + } + } + } + return destImage; + }, + + /** + * hermiteFastResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + hermiteFastResize: function(options, oW, oH, dW, dH) { + var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, + ratioWHalf = ceil(ratioW / 2), + ratioHHalf = ceil(ratioH / 2), + img = options.imageData, data = img.data, + img2 = options.ctx.createImageData(dW, dH), data2 = img2.data; + for (var j = 0; j < dH; j++) { + for (var i = 0; i < dW; i++) { + var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, + gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; + for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { + var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, + centerX = (i + 0.5) * ratioW, w0 = dy * dy; + for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { + var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, + w = sqrt(w0 + dx * dx); + /* eslint-disable max-depth */ + if (w > 1 && w < -1) { + continue; + } + //hermite filter + weight = 2 * w * w * w - 3 * w * w + 1; + if (weight > 0) { + dx = 4 * (xx + yy * oW); + //alpha + gxA += weight * data[dx + 3]; + weightsAlpha += weight; + //colors + if (data[dx + 3] < 255) { + weight = weight * data[dx + 3] / 250; + } + gxR += weight * data[dx]; + gxG += weight * data[dx + 1]; + gxB += weight * data[dx + 2]; + weights += weight; + } + /* eslint-enable max-depth */ + } + } + data2[x2] = gxR / weights; + data2[x2 + 1] = gxG / weights; + data2[x2 + 2] = gxB / weights; + data2[x2 + 3] = gxA / weightsAlpha; + } + } + return img2; + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + scaleX: this.scaleX, + scaleY: this.scaleY, + resizeType: this.resizeType, + lanczosLobes: this.lanczosLobes + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + */ + fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Contrast filter class + * @class fabric.Image.filters.Contrast + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Contrast({ + * contrast: 0.25 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Contrast', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uContrast;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\n' + + 'color.rgb = contrastF * (color.rgb - 0.5) + 0.5;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * contrast value, range from -1 to 1. + * @param {Number} contrast + * @default 0 + */ + contrast: 0, + + mainParameter: 'contrast', + + /** + * Constructor + * @memberOf fabric.Image.filters.Contrast.prototype + * @param {Object} [options] Options object + * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1) + */ + + /** + * Apply the Contrast operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + if (this.contrast === 0) { + return; + } + var imageData = options.imageData, i, len, + data = imageData.data, len = data.length, + contrast = Math.floor(this.contrast * 255), + contrastF = 259 * (contrast + 255) / (255 * (259 - contrast)); + + for (i = 0; i < len; i += 4) { + data[i] = contrastF * (data[i] - 128) + 128; + data[i + 1] = contrastF * (data[i + 1] - 128) + 128; + data[i + 2] = contrastF * (data[i + 2] - 128) + 128; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uContrast: gl.getUniformLocation(program, 'uContrast'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uContrast, this.contrast); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + */ + fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Saturate filter class + * @class fabric.Image.filters.Saturation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Saturation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Saturation({ + * saturation: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Saturation', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uSaturation;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float rgMax = max(color.r, color.g);\n' + + 'float rgbMax = max(rgMax, color.b);\n' + + 'color.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\n' + + 'color.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\n' + + 'color.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Saturation value, from -1 to 1. + * Increases/decreases the color saturation. + * A value of 0 has no effect. + * + * @param {Number} saturation + * @default + */ + saturation: 0, + + mainParameter: 'saturation', + + /** + * Constructor + * @memberOf fabric.Image.filters.Saturate.prototype + * @param {Object} [options] Options object + * @param {Number} [options.saturate=0] Value to saturate the image (-1...1) + */ + + /** + * Apply the Saturation operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.saturation === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.saturation, i, max; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + data[i] += max !== data[i] ? (max - data[i]) * adjust : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uSaturation: gl.getUniformLocation(program, 'uSaturation'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uSaturation, -this.saturation); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate + */ + fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Vibrance filter class + * @class fabric.Image.filters.Vibrance + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Vibrance#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Vibrance({ + * vibrance: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Vibrance = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Vibrance.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Vibrance', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uVibrance;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float max = max(color.r, max(color.g, color.b));\n' + + 'float avg = (color.r + color.g + color.b) / 3.0;\n' + + 'float amt = (abs(max - avg) * 2.0) * uVibrance;\n' + + 'color.r += max != color.r ? (max - color.r) * amt : 0.00;\n' + + 'color.g += max != color.g ? (max - color.g) * amt : 0.00;\n' + + 'color.b += max != color.b ? (max - color.b) * amt : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Vibrance value, from -1 to 1. + * Increases/decreases the saturation of more muted colors with less effect on saturated colors. + * A value of 0 has no effect. + * + * @param {Number} vibrance + * @default + */ + vibrance: 0, + + mainParameter: 'vibrance', + + /** + * Constructor + * @memberOf fabric.Image.filters.Vibrance.prototype + * @param {Object} [options] Options object + * @param {Number} [options.vibrance=0] Vibrance value for the image (between -1 and 1) + */ + + /** + * Apply the Vibrance operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.vibrance === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.vibrance, i, max, avg, amt; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + amt = ((Math.abs(max - avg) * 2 / 255) * adjust); + data[i] += max !== data[i] ? (max - data[i]) * amt : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * amt : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * amt : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uVibrance: gl.getUniformLocation(program, 'uVibrance'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uVibrance, -this.vibrance); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance + */ + fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Blur filter class + * @class fabric.Image.filters.Blur + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Blur#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Blur({ + * blur: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Blur = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blur.prototype */ { + + type: 'Blur', + + /* +'gl_FragColor = vec4(0.0);', +'gl_FragColor += texture2D(texture, vTexCoord + -7 * uDelta)*0.0044299121055113265;', +'gl_FragColor += texture2D(texture, vTexCoord + -6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + -5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + -4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + -3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + -2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + -1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord )*0.159576912161;', +'gl_FragColor += texture2D(texture, vTexCoord + 1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord + 2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + 3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + 4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + 5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + 6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + 7 * uDelta)*0.0044299121055113265;', +*/ + + /* eslint-disable max-len */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n' + + 'const float nSamples = 15.0;\n' + + 'vec3 v3offset = vec3(12.9898, 78.233, 151.7182);\n' + + 'float random(vec3 scale) {\n' + + /* use the fragment position for a different seed per-pixel */ + 'return fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = vec4(0.0);\n' + + 'float total = 0.0;\n' + + 'float offset = random(v3offset);\n' + + 'for (float t = -nSamples; t <= nSamples; t++) {\n' + + 'float percent = (t + offset - 0.5) / nSamples;\n' + + 'float weight = 1.0 - abs(percent);\n' + + 'color += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\n' + + 'total += weight;\n' + + '}\n' + + 'gl_FragColor = color / total;\n' + + '}', + /* eslint-enable max-len */ + + /** + * blur value, in percentage of image dimensions. + * specific to keep the image blur constant at different resolutions + * range between 0 and 1. + */ + blur: 0, + + mainParameter: 'blur', + + applyTo: function(options) { + if (options.webgl) { + // this aspectRatio is used to give the same blur to vertical and horizontal + this.aspectRatio = options.sourceWidth / options.sourceHeight; + options.passes++; + this._setupFrameBuffer(options); + this.horizontal = true; + this.applyToWebGL(options); + this._swapTextures(options); + this._setupFrameBuffer(options); + this.horizontal = false; + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + applyTo2d: function(options) { + // paint canvasEl with current image data. + //options.ctx.putImageData(options.imageData, 0, 0); + options.imageData = this.simpleBlur(options); + }, + + simpleBlur: function(options) { + var resources = options.filterBackend.resources, canvas1, canvas2, + width = options.imageData.width, + height = options.imageData.height; + + if (!resources.blurLayer1) { + resources.blurLayer1 = fabric.util.createCanvasElement(); + resources.blurLayer2 = fabric.util.createCanvasElement(); + } + canvas1 = resources.blurLayer1; + canvas2 = resources.blurLayer2; + if (canvas1.width !== width || canvas1.height !== height) { + canvas2.width = canvas1.width = width; + canvas2.height = canvas1.height = height; + } + var ctx1 = canvas1.getContext('2d'), + ctx2 = canvas2.getContext('2d'), + nSamples = 15, + random, percent, j, i, + blur = this.blur * 0.06 * 0.5; + + // load first canvas + ctx1.putImageData(options.imageData, 0, 0); + ctx2.clearRect(0, 0, width, height); + + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * width + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, j, random); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * height + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, random, j); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + options.ctx.drawImage(canvas1, 0, 0); + var newImageData = options.ctx.getImageData(0, 0, canvas1.width, canvas1.height); + ctx1.globalAlpha = 1; + ctx1.clearRect(0, 0, canvas1.width, canvas1.height); + return newImageData; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + delta: gl.getUniformLocation(program, 'uDelta'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var delta = this.chooseRightDelta(); + gl.uniform2fv(uniformLocations.delta, delta); + }, + + /** + * choose right value of image percentage to blur with + * @returns {Array} a numeric array with delta values + */ + chooseRightDelta: function() { + var blurScale = 1, delta = [0, 0], blur; + if (this.horizontal) { + if (this.aspectRatio > 1) { + // image is wide, i want to shrink radius horizontal + blurScale = 1 / this.aspectRatio; + } + } + else { + if (this.aspectRatio < 1) { + // image is tall, i want to shrink radius vertical + blurScale = this.aspectRatio; + } + } + blur = blurScale * this.blur * 0.12; + if (this.horizontal) { + delta[0] = blur; + } + else { + delta[1] = blur; + } + return delta; + }, + }); + + /** + * Deserialize a JSON definition of a BlurFilter into a concrete instance. + */ + filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Gamma filter class + * @class fabric.Image.filters.Gamma + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Gamma#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Gamma({ + * gamma: [1, 0.5, 2.1] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Gamma = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Gamma.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Gamma', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec3 uGamma;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec3 correction = (1.0 / uGamma);\n' + + 'color.r = pow(color.r, correction.r);\n' + + 'color.g = pow(color.g, correction.g);\n' + + 'color.b = pow(color.b, correction.b);\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.rgb *= color.a;\n' + + '}', + + /** + * Gamma array value, from 0.01 to 2.2. + * @param {Array} gamma + * @default + */ + gamma: [1, 1, 1], + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'gamma', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.gamma = [1, 1, 1]; + filters.BaseFilter.prototype.initialize.call(this, options); + }, + + /** + * Apply the Gamma operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, data = imageData.data, + gamma = this.gamma, len = data.length, + rInv = 1 / gamma[0], gInv = 1 / gamma[1], + bInv = 1 / gamma[2], i; + + if (!this.rVals) { + // eslint-disable-next-line + this.rVals = new Uint8Array(256); + // eslint-disable-next-line + this.gVals = new Uint8Array(256); + // eslint-disable-next-line + this.bVals = new Uint8Array(256); + } + + // This is an optimization - pre-compute a look-up table for each color channel + // instead of performing these pow calls for each pixel in the image. + for (i = 0, len = 256; i < len; i++) { + this.rVals[i] = Math.pow(i / 255, rInv) * 255; + this.gVals[i] = Math.pow(i / 255, gInv) * 255; + this.bVals[i] = Math.pow(i / 255, bInv) * 255; + } + for (i = 0, len = data.length; i < len; i += 4) { + data[i] = this.rVals[data[i]]; + data[i + 1] = this.gVals[data[i + 1]]; + data[i + 2] = this.bVals[data[i + 2]]; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uGamma: gl.getUniformLocation(program, 'uGamma'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform3fv(uniformLocations.uGamma, this.gamma); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma + */ + fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * A container class that knows how to apply a sequence of filters to an input image. + */ + filters.Composed = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Composed.prototype */ { + + type: 'Composed', + + /** + * A non sparse array of filters to apply + */ + subFilters: [], + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.subFilters = this.subFilters.slice(0); + }, + + /** + * Apply this container's filters to the input image provided. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be applied. + */ + applyTo: function(options) { + options.passes += this.subFilters.length - 1; + this.subFilters.forEach(function(filter) { + filter.applyTo(options); + }); + }, + + /** + * Serialize this filter into JSON. + * + * @returns {Object} A JSON representation of this filter. + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + subFilters: this.subFilters.map(function(filter) { return filter.toObject(); }), + }); + }, + + isNeutralState: function() { + return !this.subFilters.some(function(filter) { return !filter.isNeutralState(); }); + } + }); + + /** + * Deserialize a JSON definition of a ComposedFilter into a concrete instance. + */ + fabric.Image.filters.Composed.fromObject = function(object, callback) { + var filters = object.subFilters || [], + subFilters = filters.map(function(filter) { + return new fabric.Image.filters[filter.type](filter); + }), + instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); + callback && callback(instance); + return instance; + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * HueRotation filter class + * @class fabric.Image.filters.HueRotation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.HueRotation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.HueRotation({ + * rotation: -0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.HueRotation = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.HueRotation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'HueRotation', + + /** + * HueRotation value, from -1 to 1. + * the unit is radians + * @param {Number} myParameter + * @default + */ + rotation: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'rotation', + + calculateMatrix: function() { + var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad), + aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos; + this.matrix = [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ]; + this.matrix[0] = cos + OneMinusCos / 3; + this.matrix[1] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[2] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[5] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[6] = cos + aThird * OneMinusCos; + this.matrix[7] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[10] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[11] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[12] = cos + aThird * OneMinusCos; + }, + + /** + * HueRotation isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function(options) { + this.calculateMatrix(); + return filters.BaseFilter.prototype.isNeutralState.call(this, options); + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + this.calculateMatrix(); + filters.BaseFilter.prototype.applyTo.call(this, options); + }, + + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation + */ + fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + clone = fabric.util.object.clone; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + + var additionalProps = + ('fontFamily fontWeight fontSize text underline overline linethrough' + + ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles' + + ' direction path pathStartOffset pathSide').split(' '); + + /** + * Text class + * @class fabric.Text + * @extends fabric.Object + * @return {fabric.Text} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} + * @see {@link fabric.Text#initialize} for constructor definition + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { + + /** + * Properties which when set cause object to change dimensions + * @type Array + * @private + */ + _dimensionAffectingProps: [ + 'fontSize', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'text', + 'charSpacing', + 'textAlign', + 'styles', + 'path', + 'pathStartOffset', + 'pathSide' + ], + + /** + * @private + */ + _reNewline: /\r?\n/, + + /** + * Use this regular expression to filter for whitespaces that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpacesAndTabs: /[ \t\r]/g, + + /** + * Use this regular expression to filter for whitespace that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpaceAndTab: /[ \t\r]/, + + /** + * Use this regular expression to filter consecutive groups of non spaces. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reWords: /\S+/g, + + /** + * Type of an object + * @type String + * @default + */ + type: 'text', + + /** + * Font size (in pixels) + * @type Number + * @default + */ + fontSize: 40, + + /** + * Font weight (e.g. bold, normal, 400, 600, 800) + * @type {(Number|String)} + * @default + */ + fontWeight: 'normal', + + /** + * Font family + * @type String + * @default + */ + fontFamily: 'Times New Roman', + + /** + * Text decoration underline. + * @type Boolean + * @default + */ + underline: false, + + /** + * Text decoration overline. + * @type Boolean + * @default + */ + overline: false, + + /** + * Text decoration linethrough. + * @type Boolean + * @default + */ + linethrough: false, + + /** + * Text alignment. Possible values: "left", "center", "right", "justify", + * "justify-left", "justify-center" or "justify-right". + * @type String + * @default + */ + textAlign: 'left', + + /** + * Font style . Possible values: "", "normal", "italic" or "oblique". + * @type String + * @default + */ + fontStyle: 'normal', + + /** + * Line height + * @type Number + * @default + */ + lineHeight: 1.16, + + /** + * Superscript schema object (minimum overlap) + * @type {Object} + * @default + */ + superscript: { + size: 0.60, // fontSize factor + baseline: -0.35 // baseline-shift factor (upwards) + }, + + /** + * Subscript schema object (minimum overlap) + * @type {Object} + * @default + */ + subscript: { + size: 0.60, // fontSize factor + baseline: 0.11 // baseline-shift factor (downwards) + }, + + /** + * Background color of text lines + * @type String + * @default + */ + textBackgroundColor: '', + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps), + + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps), + + /** + * When defined, an object is rendered via stroke and this property specifies its color. + * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 + * @type String + * @default + */ + stroke: null, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * fabric.Path that the text should follow. + * since 4.6.0 the path will be drawn automatically. + * if you want to make the path visible, give it a stroke and strokeWidth or fill value + * if you want it to be hidden, assign visible = false to the path. + * This feature is in BETA, and SVG import/export is not yet supported. + * @type fabric.Path + * @example + * var textPath = new fabric.Text('Text on a path', { + * top: 150, + * left: 150, + * textAlign: 'center', + * charSpacing: -50, + * path: new fabric.Path('M 0 0 C 50 -100 150 -100 200 0', { + * strokeWidth: 1, + * visible: false + * }), + * pathSide: 'left', + * pathStartOffset: 0 + * }); + * @default + */ + path: null, + + /** + * Offset amount for text path starting position + * Only used when text has a path + * @type Number + * @default + */ + pathStartOffset: 0, + + /** + * Which side of the path the text should be drawn on. + * Only used when text has a path + * @type {String} 'left|right' + * @default + */ + pathSide: 'left', + + /** + * @private + */ + _fontSizeFraction: 0.222, + + /** + * @private + */ + offsets: { + underline: 0.10, + linethrough: -0.315, + overline: -0.88 + }, + + /** + * Text Line proportion to font Size (in pixels) + * @type Number + * @default + */ + _fontSizeMult: 1.13, + + /** + * additional space between characters + * expressed in thousands of em unit + * @type Number + * @default + */ + charSpacing: 0, + + /** + * Object containing character styles - top-level properties -> line numbers, + * 2nd-level properties - character numbers + * @type Object + * @default + */ + styles: null, + + /** + * Reference to a context to measure text char or couple of chars + * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas + * once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every + * text object created. + * @type {CanvasRenderingContext2D} + * @default + */ + _measuringContext: null, + + /** + * Baseline shift, styles only, keep at 0 for the main text object + * @type {Number} + * @default + */ + deltaY: 0, + + /** + * WARNING: EXPERIMENTAL. NOT SUPPORTED YET + * determine the direction of the text. + * This has to be set manually together with textAlign and originX for proper + * experience. + * some interesting link for the future + * https://www.w3.org/International/questions/qa-bidi-unicode-controls + * @since 4.5.0 + * @type {String} 'ltr|rtl' + * @default + */ + direction: 'ltr', + + /** + * Array of properties that define a style unit (of 'styles'). + * @type {Array} + * @default + */ + _styleProperties: [ + 'stroke', + 'strokeWidth', + 'fill', + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'underline', + 'overline', + 'linethrough', + 'deltaY', + 'textBackgroundColor', + ], + + /** + * contains characters bounding boxes + */ + __charBounds: [], + + /** + * use this size when measuring text. To avoid IE11 rounding errors + * @type {Number} + * @default + * @readonly + * @private + */ + CACHE_FONT_SIZE: 400, + + /** + * contains the min text width to avoid getting 0 + * @type {Number} + * @default + */ + MIN_TEXT_WIDTH: 2, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.text = text; + this.__skipDimension = true; + this.callSuper('initialize', options); + if (this.path) { + this.setPathInfo(); + } + this.__skipDimension = false; + this.initDimensions(); + this.setCoords(); + this.setupState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * If text has a path, it will add the extra information needed + * for path and text calculations + * @return {fabric.Text} thisArg + */ + setPathInfo: function() { + var path = this.path; + if (path) { + path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path); + } + }, + + /** + * Return a context for measurement of text string. + * if created it gets stored for reuse + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + getMeasuringContext: function() { + // if we did not return we have to measure something. + if (!fabric._measuringContext) { + fabric._measuringContext = this.canvas && this.canvas.contextCache || + fabric.util.createCanvasElement().getContext('2d'); + } + return fabric._measuringContext; + }, + + /** + * @private + * Divides text into lines of text and lines of graphemes. + */ + _splitText: function() { + var newLines = this._splitTextIntoLines(this.text); + this.textLines = newLines.lines; + this._textLines = newLines.graphemeLines; + this._unwrappedTextLines = newLines._unwrappedLines; + this._text = newLines.graphemeText; + return newLines; + }, + + /** + * Initialize or update text dimensions. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this._splitText(); + this._clearCache(); + if (this.path) { + this.width = this.path.width; + this.height = this.path.height; + } + else { + this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; + this.height = this.calcTextHeight(); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Enlarge space boxes and shift the others + */ + enlargeSpaces: function() { + var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { + continue; + } + accumulatedSpace = 0; + line = this._textLines[i]; + currentLineWidth = this.getLineWidth(i); + if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { + numberOfSpaces = spaces.length; + diffSpace = (this.width - currentLineWidth) / numberOfSpaces; + for (var j = 0, jlen = line.length; j <= jlen; j++) { + charBound = this.__charBounds[i][j]; + if (this._reSpaceAndTab.test(line[j])) { + charBound.width += diffSpace; + charBound.kernedWidth += diffSpace; + charBound.left += accumulatedSpace; + accumulatedSpace += diffSpace; + } + else { + charBound.left += accumulatedSpace; + } + } + } + } + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + return lineIndex === this._textLines.length - 1; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * It return always for text and Itext. + * @return Number + */ + missingNewlineOffset: function() { + return 1; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @param {Object} dim.x width of object to be cached + * @param {Object} dim.y height of object to be cached + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var dims = this.callSuper('_getCacheCanvasDimensions'); + var fontSize = this.fontSize; + dims.width += fontSize * dims.zoomX; + dims.height += fontSize * dims.zoomY; + return dims; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var path = this.path; + path && !path.isNotVisible() && path._render(ctx); + this._setTextStyles(ctx); + this._renderTextLinesBackground(ctx); + this._renderTextDecoration(ctx, 'underline'); + this._renderText(ctx); + this._renderTextDecoration(ctx, 'overline'); + this._renderTextDecoration(ctx, 'linethrough'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderTextStroke(ctx); + this._renderTextFill(ctx); + } + else { + this._renderTextFill(ctx); + this._renderTextStroke(ctx); + } + }, + + /** + * Set the font parameter of the context with the object properties or with charStyle + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [charStyle] object with font style properties + * @param {String} [charStyle.fontFamily] Font Family + * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix ) + * @param {String} [charStyle.fontWeight] Font weight + * @param {String} [charStyle.fontStyle] Font style (italic|normal) + */ + _setTextStyles: function(ctx, charStyle, forMeasuring) { + ctx.textBaseline = 'alphabetic'; + ctx.font = this._getFontDeclaration(charStyle, forMeasuring); + }, + + /** + * calculate and return the text Width measuring each line. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {Number} Maximum width of fabric.Text object + */ + calcTextWidth: function() { + var maxWidth = this.getLineWidth(0); + + for (var i = 1, len = this._textLines.length; i < len; i++) { + var currentLineWidth = this.getLineWidth(i); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {String} method Method name ("fillText" or "strokeText") + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Text to render + * @param {Number} left Left position of text + * @param {Number} top Top position of text + * @param {Number} lineIndex Index of a line in a text + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + this._renderChars(method, ctx, line, left, top, lineIndex); + }, + + /** + * Renders the text background for lines, taking care of style + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextLinesBackground: function(ctx) { + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) { + return; + } + var heightOfLine, + lineLeftOffset, originalFill = ctx.fillStyle, + line, lastColor, + leftOffset = this._getLeftOffset(), + lineTopOffset = this._getTopOffset(), + boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path, + drawStart; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) { + lineTopOffset += heightOfLine; + continue; + } + line = this._textLines[i]; + lineLeftOffset = this._getLineLeftOffset(i); + boxWidth = 0; + boxStart = 0; + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillStyle = currentColor; + currentColor && ctx.fillRect( + -charBox.width / 2, + -heightOfLine / this.lineHeight * (1 - this._fontSizeFraction), + charBox.width, + heightOfLine / this.lineHeight + ); + ctx.restore(); + } + else if (currentColor !== lastColor) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = lastColor; + lastColor && ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + if (currentColor && !path) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentColor; + ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + } + lineTopOffset += heightOfLine; + } + ctx.fillStyle = originalFill; + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {Object} decl style declaration for cache + * @param {String} decl.fontFamily fontFamily + * @param {String} decl.fontStyle fontStyle + * @param {String} decl.fontWeight fontWeight + * @return {Object} reference to cache + */ + getFontCache: function(decl) { + var fontFamily = decl.fontFamily.toLowerCase(); + if (!fabric.charWidthsCache[fontFamily]) { + fabric.charWidthsCache[fontFamily] = { }; + } + var cache = fabric.charWidthsCache[fontFamily], + cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase(); + if (!cache[cacheProp]) { + cache[cacheProp] = { }; + } + return cache[cacheProp]; + }, + + /** + * measure and return the width of a single character. + * possibly overridden to accommodate different measure logic or + * to hook some external lib for character measurement + * @private + * @param {String} _char, char to be measured + * @param {Object} charStyle style of char to be measured + * @param {String} [previousChar] previous char + * @param {Object} [prevCharStyle] style of previous char + */ + _measureChar: function(_char, charStyle, previousChar, prevCharStyle) { + // first i try to return from cache + var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle), + previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char, + stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth, + fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth; + + if (previousChar && fontCache[previousChar] !== undefined) { + previousWidth = fontCache[previousChar]; + } + if (fontCache[_char] !== undefined) { + kernedWidth = width = fontCache[_char]; + } + if (stylesAreEqual && fontCache[couple] !== undefined) { + coupleWidth = fontCache[couple]; + kernedWidth = coupleWidth - previousWidth; + } + if (width === undefined || previousWidth === undefined || coupleWidth === undefined) { + var ctx = this.getMeasuringContext(); + // send a TRUE to specify measuring font size CACHE_FONT_SIZE + this._setTextStyles(ctx, charStyle, true); + } + if (width === undefined) { + kernedWidth = width = ctx.measureText(_char).width; + fontCache[_char] = width; + } + if (previousWidth === undefined && stylesAreEqual && previousChar) { + previousWidth = ctx.measureText(previousChar).width; + fontCache[previousChar] = previousWidth; + } + if (stylesAreEqual && coupleWidth === undefined) { + // we can measure the kerning couple and subtract the width of the previous character + coupleWidth = ctx.measureText(couple).width; + fontCache[couple] = coupleWidth; + kernedWidth = coupleWidth - previousWidth; + } + return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier }; + }, + + /** + * Computes height of character at given position + * @param {Number} line the line index number + * @param {Number} _char the character index number + * @return {Number} fontSize of the character + */ + getHeightOfChar: function(line, _char) { + return this.getValueOfPropertyAt(line, _char, 'fontSize'); + }, + + /** + * measure a text line measuring all characters. + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + measureLine: function(lineIndex) { + var lineInfo = this._measureLine(lineIndex); + if (this.charSpacing !== 0) { + lineInfo.width -= this._getWidthOfCharSpacing(); + } + if (lineInfo.width < 0) { + lineInfo.width = 0; + } + return lineInfo; + }, + + /** + * measure every grapheme of a line, populating __charBounds + * @param {Number} lineIndex + * @return {Object} object.width total width of characters + * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs + */ + _measureLine: function(lineIndex) { + var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, + graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length), + positionInPath = 0, startingPoint, totalPathLength, path = this.path, + reverse = this.pathSide === 'right'; + + this.__charBounds[lineIndex] = lineBounds; + for (i = 0; i < line.length; i++) { + grapheme = line[i]; + graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); + lineBounds[i] = graphemeInfo; + width += graphemeInfo.kernedWidth; + prevGrapheme = grapheme; + } + // this latest bound box represent the last character of the line + // to simplify cursor handling in interactive mode. + lineBounds[i] = { + left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, + width: 0, + kernedWidth: 0, + height: this.fontSize + }; + if (path) { + totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length; + startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); + startingPoint.x += path.pathOffset.x; + startingPoint.y += path.pathOffset.y; + switch (this.textAlign) { + case 'left': + positionInPath = reverse ? (totalPathLength - width) : 0; + break; + case 'center': + positionInPath = (totalPathLength - width) / 2; + break; + case 'right': + positionInPath = reverse ? 0 : (totalPathLength - width); + break; + //todo - add support for justify + } + positionInPath += this.pathStartOffset * (reverse ? -1 : 1); + for (i = reverse ? line.length - 1 : 0; + reverse ? i >= 0 : i < line.length; + reverse ? i-- : i++) { + graphemeInfo = lineBounds[i]; + if (positionInPath > totalPathLength) { + positionInPath %= totalPathLength; + } + else if (positionInPath < 0) { + positionInPath += totalPathLength; + } + // it would probably much faster to send all the grapheme position for a line + // and calculate path position/angle at once. + this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint); + positionInPath += graphemeInfo.kernedWidth; + } + } + return { width: width, numOfSpaces: numOfSpaces }; + }, + + /** + * Calculate the angle and the left,top position of the char that follow a path. + * It appends it to graphemeInfo to be reused later at rendering + * @private + * @param {Number} positionInPath to be measured + * @param {Object} graphemeInfo current grapheme box information + * @param {Object} startingPoint position of the point + */ + _setGraphemeOnPath: function(positionInPath, graphemeInfo, startingPoint) { + var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2, + path = this.path; + + // we are at currentPositionOnPath. we want to know what point on the path is. + var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo); + graphemeInfo.renderLeft = info.x - startingPoint.x; + graphemeInfo.renderTop = info.y - startingPoint.y; + graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); + }, + + /** + * Measure and return the info of a single grapheme. + * needs the the info of previous graphemes already filled + * @private + * @param {String} grapheme to be measured + * @param {Number} lineIndex index of the line where the char is + * @param {Number} charIndex position in the line + * @param {String} [prevGrapheme] character preceding the one to be measured + */ + _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { + var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), + prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { }, + info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), + kernedWidth = info.kernedWidth, + width = info.width, charSpacing; + + if (this.charSpacing !== 0) { + charSpacing = this._getWidthOfCharSpacing(); + width += charSpacing; + kernedWidth += charSpacing; + } + + var box = { + width: width, + left: 0, + height: style.fontSize, + kernedWidth: kernedWidth, + deltaY: style.deltaY, + }; + if (charIndex > 0 && !skipLeft) { + var previousBox = this.__charBounds[lineIndex][charIndex - 1]; + box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; + } + return box; + }, + + /** + * Calculate height of line at 'lineIndex' + * @param {Number} lineIndex index of line to calculate + * @return {Number} + */ + getHeightOfLine: function(lineIndex) { + if (this.__lineHeights[lineIndex]) { + return this.__lineHeights[lineIndex]; + } + + var line = this._textLines[lineIndex], + // char 0 is measured before the line cycle because it nneds to char + // emptylines + maxHeight = this.getHeightOfChar(lineIndex, 0); + for (var i = 1, len = line.length; i < len; i++) { + maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); + } + + return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; + }, + + /** + * Calculate text box height + */ + calcTextHeight: function() { + var lineHeight, height = 0; + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineHeight = this.getHeightOfLine(i); + height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); + } + return height; + }, + + /** + * @private + * @return {Number} Left offset + */ + _getLeftOffset: function() { + return this.direction === 'ltr' ? -this.width / 2 : this.width / 2; + }, + + /** + * @private + * @return {Number} Top offset + */ + _getTopOffset: function() { + return -this.height / 2; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} method Method name ("fillText" or "strokeText") + */ + _renderTextCommon: function(ctx, method) { + ctx.save(); + var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); + for (var i = 0, len = this._textLines.length; i < len; i++) { + var heightOfLine = this.getHeightOfLine(i), + maxHeight = heightOfLine / this.lineHeight, + leftOffset = this._getLineLeftOffset(i); + this._renderTextLine( + method, + ctx, + this._textLines[i], + left + leftOffset, + top + lineHeights + maxHeight, + i + ); + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextFill: function(ctx) { + if (!this.fill && !this.styleHas('fill')) { + return; + } + + this._renderTextCommon(ctx, 'fillText'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextStroke: function(ctx) { + if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + this._setLineDash(ctx, this.strokeDashArray); + ctx.beginPath(); + this._renderTextCommon(ctx, 'strokeText'); + ctx.closePath(); + ctx.restore(); + }, + + /** + * @private + * @param {String} method fillText or strokeText. + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} line Content of the line, splitted in an array by grapheme + * @param {Number} left + * @param {Number} top + * @param {Number} lineIndex + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, + boxWidth = 0, + timeToRender, + path = this.path, + shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, + isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, + drawingLeft; + + ctx.save(); + top -= lineHeight * this._fontSizeFraction / this.lineHeight; + if (shortCut) { + // render all the line in one pass without checking + // drawingLeft = isLtr ? left : left - this.getLineWidth(lineIndex); + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight); + ctx.restore(); + return; + } + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing || path; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + left += sign * (charBox.kernedWidth - charBox.width); + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = this._hasStyleChanged(actualStyle, nextStyle); + } + if (timeToRender) { + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight); + ctx.restore(); + } + else { + drawingLeft = left; + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top, lineHeight); + } + charsToRender = ''; + actualStyle = nextStyle; + left += sign * boxWidth; + boxWidth = 0; + } + } + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {fabric.Gradient} filler a fabric gradient instance + * @return {CanvasPattern} a pattern to use as fill/stroke style + */ + _applyPatternGradientTransformText: function(filler) { + var pCanvas = fabric.util.createCanvasElement(), pCtx, + // TODO: verify compatibility with strokeUniform + width = this.width + this.strokeWidth, height = this.height + this.strokeWidth; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.fillStyle = filler.toLive(pCtx); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fill(); + return pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + handleFiller: function(ctx, property, filler) { + var offsetX, offsetY; + if (filler.toLive) { + if (filler.gradientUnits === 'percentage' || filler.gradientTransform || filler.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + offsetX = -this.width / 2; + offsetY = -this.height / 2; + ctx.translate(offsetX, offsetY); + ctx[property] = this._applyPatternGradientTransformText(filler); + return { offsetX: offsetX, offsetY: offsetY }; + } + else { + // is a simple gradient or pattern + ctx[property] = filler.toLive(ctx, this); + return this._applyPatternGradientTransform(ctx, filler); + } + } + else { + // is a color + ctx[property] = filler; + } + return { offsetX: 0, offsetY: 0 }; + }, + + _setStrokeStyles: function(ctx, decl) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = this.strokeLineCap; + ctx.lineDashOffset = this.strokeDashOffset; + ctx.lineJoin = this.strokeLineJoin; + ctx.miterLimit = this.strokeMiterLimit; + return this.handleFiller(ctx, 'strokeStyle', decl.stroke); + }, + + _setFillStyles: function(ctx, decl) { + return this.handleFiller(ctx, 'fillStyle', decl.fill); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {String} _char + * @param {Number} left Left coordinate + * @param {Number} top Top coordinate + * @param {Number} lineHeight Height of the line + */ + _renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) { + var decl = this._getStyleDeclaration(lineIndex, charIndex), + fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), + shouldFill = method === 'fillText' && fullDecl.fill, + shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth, + fillOffsets, strokeOffsets; + + if (!shouldStroke && !shouldFill) { + return; + } + ctx.save(); + + shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl)); + shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl)); + + ctx.font = this._getFontDeclaration(fullDecl); + + + if (decl && decl.textBackgroundColor) { + this._removeShadow(ctx); + } + if (decl && decl.deltaY) { + top += decl.deltaY; + } + shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY); + shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY); + ctx.restore(); + }, + + /** + * Turns the character into a 'superior figure' (i.e. 'superscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSuperscript: function(start, end) { + return this._setScript(start, end, this.superscript); + }, + + /** + * Turns the character into an 'inferior figure' (i.e. 'subscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSubscript: function(start, end) { + return this._setScript(start, end, this.subscript); + }, + + /** + * Applies 'schema' at given position + * @private + * @param {Number} start selection start + * @param {Number} end selection end + * @param {Number} schema + * @returns {fabric.Text} thisArg + * @chainable + */ + _setScript: function(start, end, schema) { + var loc = this.get2DCursorLocation(start, true), + fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'), + dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'), + style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline }; + this.setSelectionStyles(style, start, end); + return this; + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChanged: function(prevStyle, thisStyle) { + return prevStyle.fill !== thisStyle.fill || + prevStyle.stroke !== thisStyle.stroke || + prevStyle.strokeWidth !== thisStyle.strokeWidth || + prevStyle.fontSize !== thisStyle.fontSize || + prevStyle.fontFamily !== thisStyle.fontFamily || + prevStyle.fontWeight !== thisStyle.fontWeight || + prevStyle.fontStyle !== thisStyle.fontStyle || + prevStyle.deltaY !== thisStyle.deltaY; + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChangedForSvg: function(prevStyle, thisStyle) { + return this._hasStyleChanged(prevStyle, thisStyle) || + prevStyle.overline !== thisStyle.overline || + prevStyle.underline !== thisStyle.underline || + prevStyle.linethrough !== thisStyle.linethrough; + }, + + /** + * @private + * @param {Number} lineIndex index text line + * @return {Number} Line left offset + */ + _getLineLeftOffset: function(lineIndex) { + var lineWidth = this.getLineWidth(lineIndex), + lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, + isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); + if (textAlign === 'justify' + || (textAlign === 'justify-center' && !isEndOfWrapping) + || (textAlign === 'justify-right' && !isEndOfWrapping) + || (textAlign === 'justify-left' && !isEndOfWrapping) + ) { + return 0; + } + if (textAlign === 'center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'right') { + leftOffset = lineDiff; + } + if (textAlign === 'justify-center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'justify-right') { + leftOffset = lineDiff; + } + if (direction === 'rtl') { + leftOffset -= lineDiff; + } + return leftOffset; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = []; + this.__lineHeights = []; + this.__charBounds = []; + }, + + /** + * @private + */ + _shouldClearDimensionCache: function() { + var shouldClear = this._forceClearCache; + shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); + if (shouldClear) { + this.dirty = true; + this._forceClearCache = false; + } + return shouldClear; + }, + + /** + * Measure a single line given its index. Used to calculate the initial + * text bounding box. The values are calculated and stored in __lineWidths cache. + * @private + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + getLineWidth: function(lineIndex) { + if (this.__lineWidths[lineIndex]) { + return this.__lineWidths[lineIndex]; + } + + var width, line = this._textLines[lineIndex], lineInfo; + + if (line === '') { + width = 0; + } + else { + lineInfo = this.measureLine(lineIndex); + width = lineInfo.width; + } + this.__lineWidths[lineIndex] = width; + return width; + }, + + _getWidthOfCharSpacing: function() { + if (this.charSpacing !== 0) { + return this.fontSize * this.charSpacing / 1000; + } + return 0; + }, + + /** + * Retrieves the value of property at given character position + * @param {Number} lineIndex the line number + * @param {Number} charIndex the character number + * @param {String} property the property name + * @returns the value of 'property' + */ + getValueOfPropertyAt: function(lineIndex, charIndex, property) { + var charStyle = this._getStyleDeclaration(lineIndex, charIndex); + if (charStyle && typeof charStyle[property] !== 'undefined') { + return charStyle[property]; + } + return this[property]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextDecoration: function(ctx, type) { + if (!this[type] && !this.styleHas(type)) { + return; + } + var heightOfLine, size, _size, + lineLeftOffset, dy, _dy, + line, lastDecoration, + leftOffset = this._getLeftOffset(), + topOffset = this._getTopOffset(), top, + boxStart, boxWidth, charBox, currentDecoration, + maxHeight, currentFill, lastFill, path = this.path, + charSpacing = this._getWidthOfCharSpacing(), + offsetY = this.offsets[type]; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this[type] && !this.styleHas(type, i)) { + topOffset += heightOfLine; + continue; + } + line = this._textLines[i]; + maxHeight = heightOfLine / this.lineHeight; + lineLeftOffset = this._getLineLeftOffset(i); + boxStart = 0; + boxWidth = 0; + lastDecoration = this.getValueOfPropertyAt(i, 0, type); + lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); + top = topOffset + maxHeight * (1 - this._fontSizeFraction); + size = this.getHeightOfChar(i, 0); + dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentDecoration = this.getValueOfPropertyAt(i, j, type); + currentFill = this.getValueOfPropertyAt(i, j, 'fill'); + _size = this.getHeightOfChar(i, j); + _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); + if (path && currentDecoration && currentFill) { + ctx.save(); + ctx.fillStyle = lastFill; + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillRect( + -charBox.kernedWidth / 2, + offsetY * _size + _dy, + charBox.kernedWidth, + this.fontSize / 15 + ); + ctx.restore(); + } + else if ( + (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) + && boxWidth > 0 + ) { + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + if (lastDecoration && lastFill) { + ctx.fillStyle = lastFill; + ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth, + this.fontSize / 15 + ); + } + boxStart = charBox.left; + boxWidth = charBox.width; + lastDecoration = currentDecoration; + lastFill = currentFill; + size = _size; + dy = _dy; + } + else { + boxWidth += charBox.kernedWidth; + } + } + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentFill; + currentDecoration && currentFill && ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth - charSpacing, + this.fontSize / 15 + ); + topOffset += heightOfLine; + } + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * return font declaration string for canvas context + * @param {Object} [styleObject] object + * @returns {String} font declaration formatted for canvas context. + */ + _getFontDeclaration: function(styleObject, forMeasuring) { + var style = styleObject || this, family = this.fontFamily, + fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1; + var fontFamily = family === undefined || + family.indexOf('\'') > -1 || family.indexOf(',') > -1 || + family.indexOf('"') > -1 || fontIsGeneric + ? style.fontFamily : '"' + style.fontFamily + '"'; + return [ + // node-canvas needs "weight style", while browsers need "style weight" + // verify if this can be fixed in JSDOM + (fabric.isLikelyNode ? style.fontWeight : style.fontStyle), + (fabric.isLikelyNode ? style.fontStyle : style.fontWeight), + forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px', + fontFamily + ].join(' '); + }, + + /** + * Renders text instance on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if object is not visible + if (!this.visible) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + } + this.callSuper('render', ctx); + }, + + /** + * Returns the text as an array of lines. + * @param {String} text text to split + * @returns {Array} Lines in the text + */ + _splitTextIntoLines: function(text) { + var lines = text.split(this._reNewline), + newLines = new Array(lines.length), + newLine = ['\n'], + newText = []; + for (var i = 0; i < lines.length; i++) { + newLines[i] = fabric.util.string.graphemeSplit(lines[i]); + newText = newText.concat(newLines[i], newLine); + } + newText.pop(); + return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines }; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var allProperties = additionalProps.concat(propertiesToInclude); + var obj = this.callSuper('toObject', allProperties); + // styles will be overridden with a properly cloned structure + obj.styles = clone(this.styles, true); + if (obj.path) { + obj.path = this.path.toObject(); + } + return obj; + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + this.callSuper('set', key, value); + var needsDims = false; + var isAddingPath = false; + if (typeof key === 'object') { + for (var _key in key) { + if (_key === 'path') { + this.setPathInfo(); + } + needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1; + isAddingPath = isAddingPath || _key === 'path'; + } + } + else { + needsDims = this._dimensionAffectingProps.indexOf(key) !== -1; + isAddingPath = key === 'path'; + } + if (isAddingPath) { + this.setPathInfo(); + } + if (needsDims) { + this.initDimensions(); + this.setCoords(); + } + return this; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) + * @static + * @memberOf fabric.Text + * @see: http://www.w3.org/TR/SVG/text.html#TextElement + */ + fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( + 'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' ')); + + /** + * Default SVG font size + * @static + * @memberOf fabric.Text + */ + fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; + + /** + * Returns fabric.Text instance from an SVG element (not yet implemented) + * @static + * @memberOf fabric.Text + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Text.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES), + parsedAnchor = parsedAttributes.textAnchor || 'left'; + options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes); + + options.top = options.top || 0; + options.left = options.left || 0; + if (parsedAttributes.textDecoration) { + var textDecoration = parsedAttributes.textDecoration; + if (textDecoration.indexOf('underline') !== -1) { + options.underline = true; + } + if (textDecoration.indexOf('overline') !== -1) { + options.overline = true; + } + if (textDecoration.indexOf('line-through') !== -1) { + options.linethrough = true; + } + delete options.textDecoration; + } + if ('dx' in parsedAttributes) { + options.left += parsedAttributes.dx; + } + if ('dy' in parsedAttributes) { + options.top += parsedAttributes.dy; + } + if (!('fontSize' in options)) { + options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + + var textContent = ''; + + // The XML is not properly parsed in IE9 so a workaround to get + // textContent is through firstChild.data. Another workaround would be + // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) + if (!('textContent' in element)) { + if ('firstChild' in element && element.firstChild !== null) { + if ('data' in element.firstChild && element.firstChild.data !== null) { + textContent = element.firstChild.data; + } + } + } + else { + textContent = element.textContent; + } + + textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); + var originalStrokeWidth = options.strokeWidth; + options.strokeWidth = 0; + + var text = new fabric.Text(textContent, options), + textHeightScaleFactor = text.getScaledHeight() / text.height, + lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, + scaledDiff = lineHeightDiff * textHeightScaleFactor, + textHeight = text.getScaledHeight() + scaledDiff, + offX = 0; + /* + Adjust positioning: + x/y attributes in SVG correspond to the bottom-left corner of text bounding box + fabric output by default at top, left. + */ + if (parsedAnchor === 'center') { + offX = text.getScaledWidth() / 2; + } + if (parsedAnchor === 'right') { + offX = text.getScaledWidth(); + } + text.set({ + left: text.left - offX, + top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight, + strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1, + }); + callback(text); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Text instance from an object representation + * @static + * @memberOf fabric.Text + * @param {Object} object plain js Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created + */ + fabric.Text.fromObject = function(object, callback) { + var objectCopy = clone(object), path = object.path; + delete objectCopy.path; + return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { + if (path) { + fabric.Object._fromObject('Path', path, function(pathInstance) { + textInstance.set('path', pathInstance); + callback(textInstance); + }, 'path'); + } + else { + callback(textInstance); + } + }, 'text'); + }; + + fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text); + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return true; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Returns true if object has a style property or has it ina specified line + * This function is used to detect if a text will use a particular property or not. + * @param {String} property to check for + * @param {Number} lineIndex to check the style on + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (!this.styles || !property || property === '') { + return false; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return false; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] }; + // eslint-disable-next-line + for (var p1 in obj) { + // eslint-disable-next-line + for (var p2 in obj[p1]) { + if (typeof obj[p1][p2][property] !== 'undefined') { + return true; + } + } + } + return false; + }, + + /** + * Check if characters in a text have a value for a property + * whose value matches the textbox's value for that property. If so, + * the character-level property is deleted. If the character + * has no other properties, then it is also deleted. Finally, + * if the line containing that character has no other characters + * then it also is deleted. + * + * @param {string} property The property to compare between characters and text. + */ + cleanStyle: function(property) { + if (!this.styles || !property || property === '') { + return false; + } + var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue, + allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject; + // eslint-disable-next-line + for (var p1 in obj) { + letterCount = 0; + // eslint-disable-next-line + for (var p2 in obj[p1]) { + var styleObject = obj[p1][p2], + stylePropertyHasBeenSet = styleObject.hasOwnProperty(property); + + stylesCount++; + + if (stylePropertyHasBeenSet) { + if (!stylePropertyValue) { + stylePropertyValue = styleObject[property]; + } + else if (styleObject[property] !== stylePropertyValue) { + allStyleObjectPropertiesMatch = false; + } + + if (styleObject[property] === this[property]) { + delete styleObject[property]; + } + } + else { + allStyleObjectPropertiesMatch = false; + } + + if (Object.keys(styleObject).length !== 0) { + letterCount++; + } + else { + delete obj[p1][p2]; + } + } + + if (letterCount === 0) { + delete obj[p1]; + } + } + // if every grapheme has the same style set then + // delete those styles and set it on the parent + for (var i = 0; i < this._textLines.length; i++) { + graphemeCount += this._textLines[i].length; + } + if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) { + this[property] = stylePropertyValue; + this.removeStyle(property); + } + }, + + /** + * Remove a style property or properties from all individual character styles + * in a text object. Deletes the character style object if it contains no other style + * props. Deletes a line style object if it contains no other character styles. + * + * @param {String} props The property to remove from character styles. + */ + removeStyle: function(property) { + if (!this.styles || !property || property === '') { + return; + } + var obj = this.styles, line, lineNum, charNum; + for (lineNum in obj) { + line = obj[lineNum]; + for (charNum in line) { + delete line[charNum][property]; + if (Object.keys(line[charNum]).length === 0) { + delete line[charNum]; + } + } + if (Object.keys(line).length === 0) { + delete obj[lineNum]; + } + } + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this._getLineStyle(loc.lineIndex)) { + this._setLineStyle(loc.lineIndex); + } + + if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { + this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); + } + + fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. useful to manage styles. + */ + get2DCursorLocation: function(selectionStart, skipWrapping) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var lines = skipWrapping ? this._unwrappedTextLines : this._textLines, + len = lines.length; + for (var i = 0; i < len; i++) { + if (selectionStart <= lines[i].length) { + return { + lineIndex: i, + charIndex: selectionStart + }; + } + selectionStart -= lines[i].length + this.missingNewlineOffset(i); + } + return { + lineIndex: i - 1, + charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart + }; + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * if startIndex or endIndex are not provided, selectionStart or selectionEnd will be used. + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @param {Boolean} [complete] get full style or not + * @return {Array} styles an array with one, zero or more Style objects + */ + getSelectionStyles: function(startIndex, endIndex, complete) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + var styles = []; + for (var i = startIndex; i < endIndex; i++) { + styles.push(this.getStyleAtPosition(i, complete)); + } + return styles; + }, + + /** + * Gets style of a current selection/cursor position + * @param {Number} position to get styles at + * @param {Boolean} [complete] full style if true + * @return {Object} style Style object at a specified index + * @private + */ + getStyleAtPosition: function(position, complete) { + var loc = this.get2DCursorLocation(position), + style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : + this._getStyleDeclaration(loc.lineIndex, loc.charIndex); + return style || {}; + }, + + /** + * Sets style of a current selection, if no selection exist, do not set anything. + * @param {Object} [styles] Styles object + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles, startIndex, endIndex) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + for (var i = startIndex; i < endIndex; i++) { + this._extendStyles(i, styles); + } + /* not included in _extendStyles to avoid clearing cache more than once */ + this._forceClearCache = true; + return this; + }, + + /** + * get the reference, not a clone, of the style object for a given character + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Object} style object + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + var lineStyle = this.styles && this.styles[lineIndex]; + if (!lineStyle) { + return null; + } + return lineStyle[charIndex]; + }, + + /** + * return a new object that contains all the style property for a character + * the object returned is newly created + * @param {Number} lineIndex of the line where the character is + * @param {Number} charIndex position of the character on the line + * @return {Object} style object + */ + getCompleteStyleDeclaration: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex) || { }, + styleObject = { }, prop; + for (var i = 0; i < this._styleProperties.length; i++) { + prop = this._styleProperties[i]; + styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; + } + return styleObject; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + this.styles[lineIndex][charIndex] = style; + }, + + /** + * + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + delete this.styles[lineIndex][charIndex]; + }, + + /** + * @param {Number} lineIndex + * @return {Boolean} if the line exists or not + * @private + */ + _getLineStyle: function(lineIndex) { + return !!this.styles[lineIndex]; + }, + + /** + * Set the line style to an empty object so that is initialized + * @param {Number} lineIndex + * @private + */ + _setLineStyle: function(lineIndex) { + this.styles[lineIndex] = {}; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _deleteLineStyle: function(lineIndex) { + delete this.styles[lineIndex]; + } + }); +})(); + + +(function() { + + function parseDecoration(object) { + if (object.textDecoration) { + object.textDecoration.indexOf('underline') > -1 && (object.underline = true); + object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); + object.textDecoration.indexOf('overline') > -1 && (object.overline = true); + delete object.textDecoration; + } + } + + /** + * IText class (introduced in v1.4) Events are also fired with "text:" + * prefix when observing canvas. + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires changed + * @fires selection:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

    Supported key combinations:

    + *
    +   *   Move cursor:                    left, right, up, down
    +   *   Select character:               shift + left, shift + right
    +   *   Select text vertically:         shift + up, shift + down
    +   *   Move cursor by word:            alt + left, alt + right
    +   *   Select words:                   shift + alt + left, shift + alt + right
    +   *   Move cursor to line start/end:  cmd + left, cmd + right or home, end
    +   *   Select till start/end of line:  cmd + shift + left, cmd + shift + right or shift + home, shift + end
    +   *   Jump to start/end of text:      cmd + up, cmd + down
    +   *   Select till start/end of text:  cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
    +   *   Delete character:               backspace
    +   *   Delete word:                    alt + backspace
    +   *   Delete line:                    cmd + backspace
    +   *   Forward delete:                 delete
    +   *   Copy text:                      ctrl/cmd + c
    +   *   Paste text:                     ctrl/cmd + v
    +   *   Cut text:                       ctrl/cmd + x
    +   *   Select entire text:             ctrl/cmd + a
    +   *   Quit editing                    tab or esc
    +   * 
    + * + *

    Supported mouse/touch combination

    + *
    +   *   Position cursor:                click/touch
    +   *   Create selection:               click/touch & drag
    +   *   Create selection:               click & shift + click
    +   *   Select word:                    double click
    +   *   Select line:                    triple click
    +   * 
    + */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', + + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Number + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Number + * @default + */ + selectionEnd: 0, + + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', + + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, + + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, + + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of text cursor color in editing mode. + * if not set (default) will take color from the text. + * if set to a color value that fabric can understand, it will + * be used instead of the color of the text at the current position. + * @type String + * @default + */ + cursorColor: '', + + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Indicates whether internal text char widths can be cached + * @type Boolean + * @default + */ + caching: true, + + /** + * DOM container to append the hiddenTextarea. + * An alternative to attaching to the document.body. + * Useful to reduce laggish redraw of the full document.body tree and + * also with modals event capturing that won't let the textarea take focus. + * @type HTMLElement + * @default + */ + hiddenTextareaContainer: null, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + __widthOfSpace: [], + + /** + * Helps determining when the text is in composition, so that the cursor + * rendering is altered. + */ + inCompositionMode: false, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.callSuper('initialize', text, options); + this.initBehavior(); + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + index = Math.max(index, 0); + this._updateAndFire('selectionStart', index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + index = Math.min(index, this.text.length); + this._updateAndFire('selectionEnd', index); + }, + + /** + * @private + * @param {String} property 'selectionStart' or 'selectionEnd' + * @param {Number} index new position of property + */ + _updateAndFire: function(property, index) { + if (this[property] !== index) { + this._fireSelectionChanged(); + this[property] = index; + } + this._updateTextarea(); + }, + + /** + * Fires the even of selection changed + * @private + */ + _fireSelectionChanged: function() { + this.fire('selection:changed'); + this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + }, + + /** + * Initialize text dimensions. Render all text on given context + * or on a offscreen canvas to get the text width with measureText. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + * @private + */ + initDimensions: function() { + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this.callSuper('initDimensions'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + this.clearContextTop(); + this.callSuper('render', ctx); + // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor + // the correct position but not at every cursor animation. + this.cursorOffsetCache = { }; + this.renderCursorOrSelection(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + }, + + /** + * Prepare and clean the contextTop + */ + clearContextTop: function(skipRestore) { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { + return; + } + var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this.transform(ctx); + this._clearTextArea(ctx); + skipRestore || ctx.restore(); + }, + /** + * Renders cursor or selection (depending on what exists) + * it does on the contextTop. If contextTop is not available, do nothing. + */ + renderCursorOrSelection: function() { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { + return; + } + var boundaries = this._getCursorBoundaries(), + ctx = this.canvas.contextTop; + this.clearContextTop(true); + if (this.selectionStart === this.selectionEnd) { + this.renderCursor(boundaries, ctx); + } + else { + this.renderSelection(boundaries, ctx); + } + ctx.restore(); + }, + + _clearTextArea: function(ctx) { + // we add 4 pixel, to be sure to do not leave any pixel out + var width = this.width + 4, height = this.height + 4; + ctx.clearRect(-width / 2, -height / 2, width, height); + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(position) { + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + if (typeof position === 'undefined') { + position = this.selectionStart; + } + + var left = this._getLeftOffset(), + top = this._getTopOffset(), + offsets = this._getCursorBoundariesOffsets(position); + return { + left: left, + top: top, + leftOffset: offsets.left, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(position) { + if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { + return this.cursorOffsetCache; + } + var lineLeftOffset, + lineIndex, + charIndex, + topOffset = 0, + leftOffset = 0, + boundaries, + cursorPosition = this.get2DCursorLocation(position); + charIndex = cursorPosition.charIndex; + lineIndex = cursorPosition.lineIndex; + for (var i = 0; i < lineIndex; i++) { + topOffset += this.getHeightOfLine(i); + } + lineLeftOffset = this._getLineLeftOffset(lineIndex); + var bound = this.__charBounds[lineIndex][charIndex]; + bound && (leftOffset = bound.left); + if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) { + leftOffset -= this._getWidthOfCharSpacing(); + } + boundaries = { + top: topOffset, + left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), + }; + if (this.direction === 'rtl') { + boundaries.left *= -1; + } + this.cursorOffsetCache = boundaries; + return this.cursorOffsetCache; + }, + + /** + * Renders cursor + * @param {Object} boundaries + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderCursor: function(boundaries, ctx) { + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'), + multiplier = this.scaleX * this.canvas.getZoom(), + cursorWidth = this.cursorWidth / multiplier, + topOffset = boundaries.topOffset, + dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY'); + topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight + - charHeight * (1 - this._fontSizeFraction); + + if (this.inCompositionMode) { + this.renderSelection(boundaries, ctx); + } + ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill'); + ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; + ctx.fillRect( + boundaries.left + boundaries.leftOffset - cursorWidth / 2, + topOffset + boundaries.top + dy, + cursorWidth, + charHeight); + }, + + /** + * Renders text selection + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderSelection: function(boundaries, ctx) { + + var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart, + selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd, + isJustify = this.textAlign.indexOf('justify') !== -1, + start = this.get2DCursorLocation(selectionStart), + end = this.get2DCursorLocation(selectionEnd), + startLine = start.lineIndex, + endLine = end.lineIndex, + startChar = start.charIndex < 0 ? 0 : start.charIndex, + endChar = end.charIndex < 0 ? 0 : end.charIndex; + + for (var i = startLine; i <= endLine; i++) { + var lineOffset = this._getLineLeftOffset(i) || 0, + lineHeight = this.getHeightOfLine(i), + realLineHeight = 0, boxStart = 0, boxEnd = 0; + + if (i === startLine) { + boxStart = this.__charBounds[startLine][startChar].left; + } + if (i >= startLine && i < endLine) { + boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5? + } + else if (i === endLine) { + if (endChar === 0) { + boxEnd = this.__charBounds[endLine][endChar].left; + } + else { + var charSpacing = this._getWidthOfCharSpacing(); + boxEnd = this.__charBounds[endLine][endChar - 1].left + + this.__charBounds[endLine][endChar - 1].width - charSpacing; + } + } + realLineHeight = lineHeight; + if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) { + lineHeight /= this.lineHeight; + } + var drawStart = boundaries.left + lineOffset + boxStart, + drawWidth = boxEnd - boxStart, + drawHeight = lineHeight, extraTop = 0; + if (this.inCompositionMode) { + ctx.fillStyle = this.compositionColor || 'black'; + drawHeight = 1; + extraTop = lineHeight; + } + else { + ctx.fillStyle = this.selectionColor; + } + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - drawWidth; + } + ctx.fillRect( + drawStart, + boundaries.top + boundaries.topOffset + extraTop, + drawWidth, + drawHeight); + boundaries.topOffset += realLineHeight; + } + }, + + /** + * High level function to know the height of the cursor. + * the currentChar is the one that precedes the cursor + * Returns fontSize of char at the current cursor + * Unused from the library, is for the end user + * @return {Number} Character font size + */ + getCurrentCharFontSize: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize'); + }, + + /** + * High level function to know the color of the cursor. + * the currentChar is the one that precedes the cursor + * Returns color (fill) of char at the current cursor + * if the text object has a pattern or gradient for filler, it will return that. + * Unused by the library, is for the end user + * @return {String | fabric.Gradient | fabric.Pattern} Character color (fill) + */ + getCurrentCharColor: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fill'); + }, + + /** + * Returns the cursor position for the getCurrent.. functions + * @private + */ + _getCurrentCharIndex: function() { + var cursorPosition = this.get2DCursorLocation(this.selectionStart, true), + charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0; + return { l: cursorPosition.lineIndex, c: charIndex }; + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as argument + */ + fabric.IText.fromObject = function(object, callback) { + parseDecoration(object); + if (object.styles) { + for (var i in object.styles) { + for (var j in object.styles[i]) { + parseDecoration(object.styles[i][j]); + } + } + } + fabric.Object._fromObject('IText', object, callback, 'text'); + }; +})(); + + +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initAddedHandler(); + this.initRemovedHandler(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.mouseMoveHandler = this.mouseMoveHandler.bind(this); + }, + + onDeselect: function() { + this.isEditing && this.exitEditing(); + this.selected = false; + }, + + /** + * Initializes "added" event handler + */ + initAddedHandler: function() { + var _this = this; + this.on('added', function() { + var canvas = _this.canvas; + if (canvas) { + if (!canvas._hasITextHandlers) { + canvas._hasITextHandlers = true; + _this._initCanvasHandlers(canvas); + } + canvas._iTextInstances = canvas._iTextInstances || []; + canvas._iTextInstances.push(_this); + } + }); + }, + + initRemovedHandler: function() { + var _this = this; + this.on('removed', function() { + var canvas = _this.canvas; + if (canvas) { + canvas._iTextInstances = canvas._iTextInstances || []; + fabric.util.removeFromArray(canvas._iTextInstances, _this); + if (canvas._iTextInstances.length === 0) { + canvas._hasITextHandlers = false; + _this._removeCanvasHandlers(canvas); + } + } + }); + }, + + /** + * register canvas event to manage exiting on other instances + * @private + */ + _initCanvasHandlers: function(canvas) { + canvas._mouseUpITextHandler = function() { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.__isMousedown = false; + }); + } + }; + canvas.on('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * remove canvas event to manage exiting on other instances + * @private + */ + _removeCanvasHandlers: function(canvas) { + canvas.off('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * @private + */ + _tick: function() { + this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete'); + }, + + /** + * @private + */ + _animateCursor: function(obj, targetOpacity, duration, completeMethod) { + + var tickState; + + tickState = { + isAborted: false, + abort: function() { + this.isAborted = true; + }, + }; + + obj.animate('_currentCursorOpacity', targetOpacity, { + duration: duration, + onComplete: function() { + if (!tickState.isAborted) { + obj[completeMethod](); + } + }, + onChange: function() { + // we do not want to animate a selection, only cursor + if (obj.canvas && obj.selectionStart === obj.selectionEnd) { + obj.renderCursorOrSelection(); + } + }, + abort: function() { + return tickState.isAborted; + } + }); + return tickState; + }, + + /** + * @private + */ + _onTickComplete: function() { + + var _this = this; + + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick'); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function(restart) { + var _this = this, + delay = restart ? 0 : this.cursorDelay; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this._cursorTimeout2 = setTimeout(function() { + _this._tick(); + }, delay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + var shouldClear = this._currentTickState || this._currentTickCompleteState, + canvas = this.canvas; + this._currentTickState && this._currentTickState.abort(); + this._currentTickCompleteState && this._currentTickCompleteState.abort(); + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + // to clear just itext area we need to transform the context + // it may not be worth it + if (shouldClear && canvas) { + canvas.clearContext(canvas.contextTop || canvas.contextContainer); + } + + }, + + /** + * Selects entire text + * @return {fabric.IText} thisArg + * @chainable + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this._text.length; + this._fireSelectionChanged(); + this._updateTextarea(); + return this; + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this._text.slice(this.selectionStart, this.selectionEnd).join(''); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { + offset++; + index--; + } + } + while (/\S/.test(this._text[index]) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { + offset++; + index++; + } + } + while (/\S/.test(this._text[index]) && index < this._text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this._text[index]) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this._text[index]) && index < this._text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction 1 or -1 + * @return {Number} Index of the beginning or end of a word + */ + searchWordBoundary: function(selectionStart, direction) { + var text = this._text, + index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart, + _char = text[index], + // wrong + reNonWord = fabric.reNonWord; + + while (!reNonWord.test(_char) && index > 0 && index < text.length) { + index += direction; + _char = text[index]; + } + if (reNonWord.test(_char)) { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */ + newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + * @return {fabric.IText} thisArg + * @chainable + */ + selectLine: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.findLineBoundaryLeft(selectionStart), + newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + return this; + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function(e) { + if (this.isEditing || !this.editable) { + return; + } + + if (this.canvas) { + this.canvas.calcOffset(); + this.exitEditingOnOthers(this.canvas); + } + + this.isEditing = true; + + this.initHiddenTextarea(e); + this.hiddenTextarea.focus(); + this.hiddenTextarea.value = this.text; + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + this._textBeforeEdit = this.text; + + this._tick(); + this.fire('editing:entered'); + this._fireSelectionChanged(); + if (!this.canvas) { + return this; + } + this.canvas.fire('text:editing:entered', { target: this }); + this.initMouseMoveHandler(); + this.canvas.requestRenderAll(); + return this; + }, + + exitEditingOnOthers: function(canvas) { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.selected = false; + if (obj.isEditing) { + obj.exitEditing(); + } + }); + } + }, + + /** + * Initializes "mousemove" event handler + */ + initMouseMoveHandler: function() { + this.canvas.on('mouse:move', this.mouseMoveHandler); + }, + + /** + * @private + */ + mouseMoveHandler: function(options) { + if (!this.__isMousedown || !this.isEditing) { + return; + } + + var newSelectionStart = this.getSelectionStartFromPointer(options.e), + currentStart = this.selectionStart, + currentEnd = this.selectionEnd; + if ( + (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) + && + (currentStart === newSelectionStart || currentEnd === newSelectionStart) + ) { + return; + } + if (newSelectionStart > this.__selectionStartOnMouseDown) { + this.selectionStart = this.__selectionStartOnMouseDown; + this.selectionEnd = newSelectionStart; + } + else { + this.selectionStart = newSelectionStart; + this.selectionEnd = this.__selectionStartOnMouseDown; + } + if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { + this.restartCursorIfNeeded(); + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + } + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * convert from textarea to grapheme indexes + */ + fromStringToGraphemeSelection: function(start, end, text) { + var smallerTextStart = text.slice(0, start), + graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; + if (start === end) { + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + } + var smallerTextEnd = text.slice(start, end), + graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + }, + + /** + * convert from fabric to textarea values + */ + fromGraphemeToStringSelection: function(start, end, _text) { + var smallerTextStart = _text.slice(0, start), + graphemeStart = smallerTextStart.join('').length; + if (start === end) { + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + } + var smallerTextEnd = _text.slice(start, end), + graphemeEnd = smallerTextEnd.join('').length; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + }, + + /** + * @private + */ + _updateTextarea: function() { + this.cursorOffsetCache = { }; + if (!this.hiddenTextarea) { + return; + } + if (!this.inCompositionMode) { + var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text); + this.hiddenTextarea.selectionStart = newSelection.selectionStart; + this.hiddenTextarea.selectionEnd = newSelection.selectionEnd; + } + this.updateTextareaPosition(); + }, + + /** + * @private + */ + updateFromTextArea: function() { + if (!this.hiddenTextarea) { + return; + } + this.cursorOffsetCache = { }; + this.text = this.hiddenTextarea.value; + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + var newSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value); + this.selectionEnd = this.selectionStart = newSelection.selectionEnd; + if (!this.inCompositionMode) { + this.selectionStart = newSelection.selectionStart; + } + this.updateTextareaPosition(); + }, + + /** + * @private + */ + updateTextareaPosition: function() { + if (this.selectionStart === this.selectionEnd) { + var style = this._calcTextareaPosition(); + this.hiddenTextarea.style.left = style.left; + this.hiddenTextarea.style.top = style.top; + } + }, + + /** + * @private + * @return {Object} style contains style for hiddenTextarea + */ + _calcTextareaPosition: function() { + if (!this.canvas) { + return { x: 1, y: 1 }; + } + var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart, + boundaries = this._getCursorBoundaries(desiredPosition), + cursorLocation = this.get2DCursorLocation(desiredPosition), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight, + leftOffset = boundaries.leftOffset, + m = this.calcTransformMatrix(), + p = { + x: boundaries.left + leftOffset, + y: boundaries.top + boundaries.topOffset + charHeight + }, + retinaScaling = this.canvas.getRetinaScaling(), + upperCanvas = this.canvas.upperCanvasEl, + upperCanvasWidth = upperCanvas.width / retinaScaling, + upperCanvasHeight = upperCanvas.height / retinaScaling, + maxWidth = upperCanvasWidth - charHeight, + maxHeight = upperCanvasHeight - charHeight, + scaleX = upperCanvas.clientWidth / upperCanvasWidth, + scaleY = upperCanvas.clientHeight / upperCanvasHeight; + + p = fabric.util.transformPoint(p, m); + p = fabric.util.transformPoint(p, this.canvas.viewportTransform); + p.x *= scaleX; + p.y *= scaleY; + if (p.x < 0) { + p.x = 0; + } + if (p.x > maxWidth) { + p.x = maxWidth; + } + if (p.y < 0) { + p.y = 0; + } + if (p.y > maxHeight) { + p.y = maxHeight; + } + + // add canvas offset on document + p.x += this.canvas._offset.left; + p.y += this.canvas._offset.top; + + return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight }; + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + selectable: this.selectable, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) { + return; + } + + this.hoverCursor = this._savedProps.hoverCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.selectable = this._savedProps.selectable; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + var isTextChanged = (this._textBeforeEdit !== this.text); + var hiddenTextarea = this.hiddenTextarea; + this.selected = false; + this.isEditing = false; + + this.selectionEnd = this.selectionStart; + + if (hiddenTextarea) { + hiddenTextarea.blur && hiddenTextarea.blur(); + hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea); + } + this.hiddenTextarea = null; + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this.fire('editing:exited'); + isTextChanged && this.fire('modified'); + if (this.canvas) { + this.canvas.off('mouse:move', this.mouseMoveHandler); + this.canvas.fire('text:editing:exited', { target: this }); + isTextChanged && this.canvas.fire('object:modified', { target: this }); + } + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + for (var prop in this.styles) { + if (!this._textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * remove and reflow a style block from start to end. + * @param {Number} start linear start position for removal (included in removal) + * @param {Number} end linear end position for removal ( excluded from removal ) + */ + removeStyleFromTo: function(start, end) { + var cursorStart = this.get2DCursorLocation(start, true), + cursorEnd = this.get2DCursorLocation(end, true), + lineStart = cursorStart.lineIndex, + charStart = cursorStart.charIndex, + lineEnd = cursorEnd.lineIndex, + charEnd = cursorEnd.charIndex, + i, styleObj; + if (lineStart !== lineEnd) { + // step1 remove the trailing of lineStart + if (this.styles[lineStart]) { + for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) { + delete this.styles[lineStart][i]; + } + } + // step2 move the trailing of lineEnd to lineStart if needed + if (this.styles[lineEnd]) { + for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) { + styleObj = this.styles[lineEnd][i]; + if (styleObj) { + this.styles[lineStart] || (this.styles[lineStart] = { }); + this.styles[lineStart][charStart + i - charEnd] = styleObj; + } + } + } + // step3 detects lines will be completely removed. + for (i = lineStart + 1; i <= lineEnd; i++) { + delete this.styles[i]; + } + // step4 shift remaining lines. + this.shiftLineStyles(lineEnd, lineStart - lineEnd); + } + else { + // remove and shift left on the same line + if (this.styles[lineStart]) { + styleObj = this.styles[lineStart]; + var diff = charEnd - charStart, numericChar, _char; + for (i = charStart; i < charEnd; i++) { + delete styleObj[i]; + } + for (_char in this.styles[lineStart]) { + numericChar = parseInt(_char, 10); + if (numericChar >= charEnd) { + styleObj[numericChar - diff] = styleObj[_char]; + delete styleObj[_char]; + } + } + } + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can any number? + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by offset upward or downward + // do not clone deep. we need new array, not new style objects + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + if (!clonedStyles[numericLine - offset]) { + delete this.styles[numericLine]; + } + } + } + }, + + restartCursorIfNeeded: function() { + if (!this._currentTickState || this._currentTickState.isAborted + || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted + ) { + this.initDelayedCursor(); + } + }, + + /** + * Handle insertion of more consecutive style lines for when one or more + * newlines gets added to the text. Since current style needs to be shifted + * first we shift the current style of the number lines needed, then we add + * new lines from the last to the first. + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Number} qty number of lines to add + * @param {Array} copiedStyle Array of objects styles + */ + insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) { + var currentCharStyle, + newLineStyles = {}, + somethingAdded = false, + isEndOfLine = this._unwrappedTextLines[lineIndex].length === charIndex; + + qty || (qty = 1); + this.shiftLineStyles(lineIndex, qty); + if (this.styles[lineIndex]) { + currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1]; + } + // we clone styles of all chars + // after cursor onto the current line + for (var index in this.styles[lineIndex]) { + var numIndex = parseInt(index, 10); + if (numIndex >= charIndex) { + somethingAdded = true; + newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + if (!(isEndOfLine && charIndex === 0)) { + delete this.styles[lineIndex][index]; + } + } + } + var styleCarriedOver = false; + if (somethingAdded && !isEndOfLine) { + // if is end of line, the extra style we copied + // is probably not something we want + this.styles[lineIndex + qty] = newLineStyles; + styleCarriedOver = true; + } + if (styleCarriedOver) { + // skip the last line of since we already prepared it. + qty--; + } + // for the all the lines or all the other lines + // we clone current char style onto the next (otherwise empty) line + while (qty > 0) { + if (copiedStyle && copiedStyle[qty - 1]) { + this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty - 1]) }; + } + else if (currentCharStyle) { + this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) }; + } + else { + delete this.styles[lineIndex + qty]; + } + qty--; + } + this._forceClearCache = true; + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Number} quantity number Style object to insert, if given + * @param {Array} copiedStyle array of style objects + */ + insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) { + if (!this.styles) { + this.styles = {}; + } + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {}; + + quantity || (quantity = 1); + // shift all char styles by quantity forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex]; + // only delete the style if there was nothing moved there + if (!currentLineStylesCloned[numericIndex - quantity]) { + delete currentLineStyles[numericIndex]; + } + } + } + this._forceClearCache = true; + if (copiedStyle) { + while (quantity--) { + if (!Object.keys(copiedStyle[quantity]).length) { + continue; + } + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = {}; + } + this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]); + } + return; + } + if (!currentLineStyles) { + return; + } + var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1]; + while (newStyle && quantity--) { + this.styles[lineIndex][charIndex + quantity] = clone(newStyle); + } + }, + + /** + * Inserts style object(s) + * @param {Array} insertedText Characters at the location where style is inserted + * @param {Number} start cursor index for inserting style + * @param {Array} [copiedStyle] array of style objects to insert. + */ + insertNewStyleBlock: function(insertedText, start, copiedStyle) { + var cursorLoc = this.get2DCursorLocation(start, true), + addedLines = [0], linesLength = 0; + // get an array of how many char per lines are being added. + for (var i = 0; i < insertedText.length; i++) { + if (insertedText[i] === '\n') { + linesLength++; + addedLines[linesLength] = 0; + } + else { + addedLines[linesLength]++; + } + } + // for the first line copy the style from the current char position. + if (addedLines[0] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle); + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1); + } + linesLength && this.insertNewlineStyleObject( + cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength); + for (var i = 1; i < linesLength; i++) { + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); + } + else if (copiedStyle) { + this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0]; + } + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1); + } + // we use i outside the loop to get it like linesLength + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); + } + }, + + /** + * Set the selectionStart and selectionEnd according to the new position of cursor + * mimic the key - mouse navigation when shift is pressed. + */ + setSelectionStartEndWithShift: function(start, end, newSelection) { + if (newSelection <= start) { + if (end === start) { + this._selectionDirection = 'left'; + } + else if (this._selectionDirection === 'right') { + this._selectionDirection = 'left'; + this.selectionEnd = start; + } + this.selectionStart = newSelection; + } + else if (newSelection > start && newSelection < end) { + if (this._selectionDirection === 'right') { + this.selectionEnd = newSelection; + } + else { + this.selectionStart = newSelection; + } + } + else { + // newSelection is > selection start and end + if (end === start) { + this._selectionDirection = 'right'; + } + else if (this._selectionDirection === 'left') { + this._selectionDirection = 'right'; + this.selectionStart = end; + } + this.selectionEnd = newSelection; + } + }, + + setSelectionInBoundaries: function() { + var length = this.text.length; + if (this.selectionStart > length) { + this.selectionStart = length; + } + else if (this.selectionStart < 0) { + this.selectionStart = 0; + } + if (this.selectionEnd > length) { + this.selectionEnd = length; + } + else if (this.selectionEnd < 0) { + this.selectionEnd = 0; + } + } + }); +})(); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.__lastPointer = { }; + + this.on('mousedown', this.onMouseDown); + }, + + /** + * Default event handler to simulate triple click + * @private + */ + onMouseDown: function(options) { + if (!this.canvas) { + return; + } + this.__newClickTime = +new Date(); + var newPointer = options.pointer; + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + this.__lastIsEditing = this.isEditing; + this.__lastSelected = this.selected; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + /** + * @private + */ + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, + + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initMousedownHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, + + /** + * Default handler for double click, select a word + */ + doubleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Default handler for triple click, select a line + */ + tripleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('mousedblclick', this.doubleClickHandler); + this.on('tripleclick', this.tripleClickHandler); + }, + + /** + * Default event handler for the basic functionalities needed on _mouseDown + * can be overridden to do something different. + * Scope of this implementation is: find the click position, set selectionStart + * find selectionEnd, initialize the drawing of either cursor or selection area + * initializing a mousedDown on a text area will cancel fabricjs knowledge of + * current compositionMode. It will be set to false. + */ + _mouseDownHandler: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + + this.__isMousedown = true; + + if (this.selected) { + this.inCompositionMode = false; + this.setCursorByClick(options.e); + } + + if (this.isEditing) { + this.__selectionStartOnMouseDown = this.selectionStart; + if (this.selectionStart === this.selectionEnd) { + this.abortCursorAnimation(); + } + this.renderCursorOrSelection(); + } + }, + + /** + * Default event handler for the basic functionalities needed on mousedown:before + * can be overridden to do something different. + * Scope of this implementation is: verify the object is already selected when mousing down + */ + _mouseDownHandlerBefore: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + // we want to avoid that an object that was selected and then becomes unselectable, + // may trigger editing mode in some way. + this.selected = this === this.canvas._activeObject; + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', this._mouseDownHandler); + this.on('mousedown:before', this._mouseDownHandlerBefore); + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', this.mouseUpHandler); + }, + + /** + * standard handler for mouse up, overridable + * @private + */ + mouseUpHandler: function(options) { + this.__isMousedown = false; + if (!this.editable || this.group || + (options.transform && options.transform.actionPerformed) || + (options.e.button && options.e.button !== 1)) { + return; + } + + if (this.canvas) { + var currentActive = this.canvas._activeObject; + if (currentActive && currentActive !== this) { + // avoid running this logic when there is an active object + // this because is possible with shift click and fast clicks, + // to rapidly deselect and reselect this object and trigger an enterEdit + return; + } + } + + if (this.__lastSelected && !this.__corner) { + this.selected = false; + this.__lastSelected = false; + this.enterEditing(options.e); + if (this.selectionStart === this.selectionEnd) { + this.initDelayedCursor(true); + } + else { + this.renderCursorOrSelection(); + } + } + else { + this.selected = true; + } + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Event} e Event object + */ + setCursorByClick: function(e) { + var newSelection = this.getSelectionStartFromPointer(e), + start = this.selectionStart, end = this.selectionEnd; + if (e.shiftKey) { + this.setSelectionStartEndWithShift(start, end, newSelection); + } + else { + this.selectionStart = newSelection; + this.selectionEnd = newSelection; + } + if (this.isEditing) { + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + var mouseOffset = this.getLocalPointer(e), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + lineIndex = 0, + lineLeftOffset, + line; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (height <= mouseOffset.y) { + height += this.getHeightOfLine(i) * this.scaleY; + lineIndex = i; + if (i > 0) { + charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1); + } + } + else { + break; + } + } + lineLeftOffset = this._getLineLeftOffset(lineIndex); + width = lineLeftOffset * this.scaleX; + line = this._textLines[lineIndex]; + // handling of RTL: in order to get things work correctly, + // we assume RTL writing is mirrored compared to LTR writing. + // so in position detection we mirror the X offset, and when is time + // of rendering it, we mirror it again. + if (this.direction === 'rtl') { + mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + } + for (var j = 0, jlen = line.length; j < jlen; j++) { + prevWidth = width; + // i removed something about flipX here, check. + width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX; + if (width <= mouseOffset.x) { + charIndex++; + } + else { + break; + } + } + return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen); + }, + + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0 + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor || + distanceBtwNextCharAndCursor < 0 ? 0 : 1, + newSelectionStart = index + offset; + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this._text.length) { + newSelectionStart = this._text.length; + } + + return newSelectionStart; + } +}); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.setAttribute('autocorrect', 'off'); + this.hiddenTextarea.setAttribute('autocomplete', 'off'); + this.hiddenTextarea.setAttribute('spellcheck', 'false'); + this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', ''); + this.hiddenTextarea.setAttribute('wrap', 'off'); + var style = this._calcTextareaPosition(); + // line-height: 1px; was removed from the style to fix this: + // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 + this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + + ' paddingーtop: ' + style.fontSize + ';'; + + if (this.hiddenTextareaContainer) { + this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); + } + else { + fabric.document.body.appendChild(this.hiddenTextarea); + } + + fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); + + if (!this._clickHandlerInitialized && this.canvas) { + fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this)); + this._clickHandlerInitialized = true; + } + }, + + /** + * For functionalities on keyDown + * Map a special key to a function of the instance/prototype + * If you need different behaviour for ESC or TAB or arrows, you have to change + * this map setting the name of a function that you build on the fabric.Itext or + * your prototype. + * the map change will affect all Instances unless you need for only some text Instances + * in that case you have to clone this object and assign your Instance. + * this.keysMap = fabric.util.object.clone(this.keysMap); + * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0] + */ + keysMap: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorRight', + 36: 'moveCursorLeft', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + }, + + keysMapRtl: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorLeft', + 36: 'moveCursorRight', + 37: 'moveCursorRight', + 38: 'moveCursorUp', + 39: 'moveCursorLeft', + 40: 'moveCursorDown', + }, + + /** + * For functionalities on keyUp + ctrl || cmd + */ + ctrlKeysMapUp: { + 67: 'copy', + 88: 'cut' + }, + + /** + * For functionalities on keyDown + ctrl || cmd + */ + ctrlKeysMapDown: { + 65: 'selectAll' + }, + + onClick: function() { + // No need to trigger click event here, focus is enough to have the keyboard appear on Android + this.hiddenTextarea && this.hiddenTextarea.focus(); + }, + + /** + * Handles keydown event + * only used for arrows and combination of modifier keys. + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) { + return; + } + var keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap; + if (e.keyCode in keyMap) { + this[keyMap[e.keyCode]](e); + } + else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapDown[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.keyCode >= 33 && e.keyCode <= 40) { + // if i press an arrow key just update selection + this.inCompositionMode = false; + this.clearContextTop(); + this.renderCursorOrSelection(); + } + else { + this.canvas && this.canvas.requestRenderAll(); + } + }, + + /** + * Handles keyup event + * We handle KeyUp because ie11 and edge have difficulties copy/pasting + * if a copy/cut event fired, keyup is dismissed + * @param {Event} e Event object + */ + onKeyUp: function(e) { + if (!this.isEditing || this._copyDone || this.inCompositionMode) { + this._copyDone = false; + return; + } + if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapUp[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + this.canvas && this.canvas.requestRenderAll(); + }, + + /** + * Handles onInput event + * @param {Event} e Event object + */ + onInput: function(e) { + var fromPaste = this.fromPaste; + this.fromPaste = false; + e && e.stopPropagation(); + if (!this.isEditing) { + return; + } + // decisions about style changes. + var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText, + charCount = this._text.length, + nextCharCount = nextText.length, + removedText, insertedText, + charDiff = nextCharCount - charCount, + selectionStart = this.selectionStart, selectionEnd = this.selectionEnd, + selection = selectionStart !== selectionEnd, + copiedStyle, removeFrom, removeTo; + if (this.hiddenTextarea.value === '') { + this.styles = { }; + this.updateFromTextArea(); + this.fire('changed'); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.requestRenderAll(); + } + return; + } + + var textareaSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, + this.hiddenTextarea.selectionEnd, + this.hiddenTextarea.value + ); + var backDelete = selectionStart > textareaSelection.selectionStart; + + if (selection) { + removedText = this._text.slice(selectionStart, selectionEnd); + charDiff += selectionEnd - selectionStart; + } + else if (nextCharCount < charCount) { + if (backDelete) { + removedText = this._text.slice(selectionEnd + charDiff, selectionEnd); + } + else { + removedText = this._text.slice(selectionStart, selectionStart - charDiff); + } + } + insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd); + if (removedText && removedText.length) { + if (insertedText.length) { + // let's copy some style before deleting. + // we want to copy the style before the cursor OR the style at the cursor if selection + // is bigger than 0. + copiedStyle = this.getSelectionStyles(selectionStart, selectionStart + 1, false); + // now duplicate the style one for each inserted text. + copiedStyle = insertedText.map(function() { + // this return an array of references, but that is fine since we are + // copying the style later. + return copiedStyle[0]; + }); + } + if (selection) { + removeFrom = selectionStart; + removeTo = selectionEnd; + } + else if (backDelete) { + // detect differences between forwardDelete and backDelete + removeFrom = selectionEnd - removedText.length; + removeTo = selectionEnd; + } + else { + removeFrom = selectionEnd; + removeTo = selectionEnd + removedText.length; + } + this.removeStyleFromTo(removeFrom, removeTo); + } + if (insertedText.length) { + if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) { + copiedStyle = fabric.copiedTextStyle; + } + this.insertNewStyleBlock(insertedText, selectionStart, copiedStyle); + } + this.updateFromTextArea(); + this.fire('changed'); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.requestRenderAll(); + } + }, + /** + * Composition start + */ + onCompositionStart: function() { + this.inCompositionMode = true; + }, + + /** + * Composition end + */ + onCompositionEnd: function() { + this.inCompositionMode = false; + }, + + // /** + // * Composition update + // */ + onCompositionUpdate: function(e) { + this.compositionStart = e.target.selectionStart; + this.compositionEnd = e.target.selectionEnd; + this.updateTextareaPosition(); + }, + + /** + * Copies selected text + * @param {Event} e Event object + */ + copy: function() { + if (this.selectionStart === this.selectionEnd) { + //do not cut-copy if no selection + return; + } + + fabric.copiedText = this.getSelectedText(); + if (!fabric.disableStyleCopyPaste) { + fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true); + } + else { + fabric.copiedTextStyle = null; + } + this._copyDone = true; + }, + + /** + * Pastes text + * @param {Event} e Event object + */ + paste: function() { + this.fromPaste = true; + }, + + /** + * @private + * @param {Event} e Event object + * @return {Object} Clipboard data object + */ + _getClipboardData: function(e) { + return (e && e.clipboardData) || fabric.window.clipboardData; + }, + + /** + * Finds the width in pixels before the cursor on the same line + * @private + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Number} widthBeforeCursor width before cursor + */ + _getWidthBeforeCursor: function(lineIndex, charIndex) { + var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound; + + if (charIndex > 0) { + bound = this.__charBounds[lineIndex][charIndex - 1]; + widthBeforeCursor += bound.left + bound.width; + } + return widthBeforeCursor; + }, + + /** + * Gets start offset of a selection + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + // if on last line, down cursor goes to end of line + if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) { + // move to the end of a text + return this._text.length - selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor), + textAfterCursor = this._textLines[lineIndex].slice(charIndex); + return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex); + }, + + /** + * private + * Helps finding if the offset should be counted from Start or End + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + _getSelectionForOffset: function(e, isRight) { + if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) { + return this.selectionEnd; + } + else { + return this.selectionStart; + } + }, + + /** + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getUpCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + if (lineIndex === 0 || e.metaKey || e.keyCode === 33) { + // if on first line, up cursor goes to start of line + return -selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), + textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), + missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1); + // return a negative offset + return -this._textLines[lineIndex - 1].length + + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset); + }, + + /** + * for a given width it founds the matching character. + * @private + */ + _getIndexOnLine: function(lineIndex, width) { + + var line = this._textLines[lineIndex], + lineLeftOffset = this._getLineLeftOffset(lineIndex), + widthOfCharsOnLine = lineLeftOffset, + indexOnLine = 0, charWidth, foundMatch; + + for (var j = 0, jlen = line.length; j < jlen; j++) { + charWidth = this.__charBounds[lineIndex][j].width; + widthOfCharsOnLine += charWidth; + if (widthOfCharsOnLine > width) { + foundMatch = true; + var leftEdge = widthOfCharsOnLine - charWidth, + rightEdge = widthOfCharsOnLine, + offsetFromLeftEdge = Math.abs(leftEdge - width), + offsetFromRightEdge = Math.abs(rightEdge - width); + + indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + break; + } + } + + // reached end + if (!foundMatch) { + indexOnLine = line.length - 1; + } + + return indexOnLine; + }, + + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { + return; + } + this._moveCursorUpOrDown('Down', e); + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorUpOrDown('Up', e); + }, + + /** + * Moves cursor up or down, fires the events + * @param {String} direction 'Up' or 'Down' + * @param {Event} e Event object + */ + _moveCursorUpOrDown: function(direction, e) { + // getUpCursorOffset + // getDownCursorOffset + var action = 'get' + direction + 'CursorOffset', + offset = this[action](e, this._selectionDirection === 'right'); + if (e.shiftKey) { + this.moveCursorWithShift(offset); + } + else { + this.moveCursorWithoutShift(offset); + } + if (offset !== 0) { + this.setSelectionInBoundaries(); + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor with shift + * @param {Number} offset + */ + moveCursorWithShift: function(offset) { + var newSelection = this._selectionDirection === 'left' + ? this.selectionStart + offset + : this.selectionEnd + offset; + this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); + return offset !== 0; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorWithoutShift: function(offset) { + if (offset < 0) { + this.selectionStart += offset; + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += offset; + this.selectionStart = this.selectionEnd; + } + return offset !== 0; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorLeftOrRight('Left', e); + }, + + /** + * @private + * @return {Boolean} true if a change happened + */ + _move: function(e, prop, direction) { + var newValue; + if (e.altKey) { + newValue = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += direction === 'Left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + return this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + var change = true; + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) { + change = this._moveLeft(e, 'selectionStart'); + + } + this.selectionEnd = this.selectionStart; + return change; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + return this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0){ + this._selectionDirection = 'left'; + return this._moveLeft(e, 'selectionStart'); + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { + return; + } + this._moveCursorLeftOrRight('Right', e); + }, + + /** + * Moves cursor right or Left, fires event + * @param {String} direction 'Left', 'Right' + * @param {Event} e Event object + */ + _moveCursorLeftOrRight: function(direction, e) { + var actionName = 'moveCursor' + direction + 'With'; + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + actionName += 'Shift'; + } + else { + actionName += 'outShift'; + } + if (this[actionName](e)) { + this.abortCursorAnimation(); + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + return this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + return this._moveRight(e, 'selectionEnd'); + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e Event object + */ + moveCursorRightWithoutShift: function(e) { + var changed = true; + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + return changed; + }, + + /** + * Removes characters from start/end + * start/end ar per grapheme position in _text array. + * + * @param {Number} start + * @param {Number} end default to start + 1 + */ + removeChars: function(start, end) { + if (typeof end === 'undefined') { + end = start + 1; + } + this.removeStyleFromTo(start, end); + this._text.splice(start, end - start); + this.text = this._text.join(''); + this.set('dirty', true); + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this._removeExtraneousStyles(); + }, + + /** + * insert characters at start position, before start position. + * start equal 1 it means the text get inserted between actual grapheme 0 and 1 + * if style array is provided, it must be as the same length of text in graphemes + * if end is provided and is bigger than start, old text is replaced. + * start/end ar per grapheme position in _text array. + * + * @param {String} text text to insert + * @param {Array} style array of style objects + * @param {Number} start + * @param {Number} end default to start + 1 + */ + insertChars: function(text, style, start, end) { + if (typeof end === 'undefined') { + end = start; + } + if (end > start) { + this.removeStyleFromTo(start, end); + } + var graphemes = fabric.util.string.graphemeSplit(text); + this.insertNewStyleBlock(graphemes, start, style); + this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); + this.text = this._text.join(''); + this.set('dirty', true); + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this._removeExtraneousStyles(); + }, + +}); + + +/* _TO_SVG_START_ */ +(function() { + var toFixed = fabric.util.toFixed, + multipleSpacesRegex = / +/g; + + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function() { + var offsets = this._getSVGLeftTopOffsets(), + textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); + return this._wrapSVGTextAndBg(textAndBg); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + return this._createBaseSVGMarkup( + this._toSVG(), + { reviver: reviver, noStyle: true, withShadow: true } + ); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function() { + return { + textLeft: -this.width / 2, + textTop: -this.height / 2, + lineTop: this.getHeightOfLine(0) + }; + }, + + /** + * @private + */ + _wrapSVGTextAndBg: function(textAndBg) { + var noShadow = true, + textDecoration = this.getSvgTextDecoration(this); + return [ + textAndBg.textBgRects.join(''), + '\t\t', + textAndBg.textSpans.join(''), + '\n' + ]; + }, + + /** + * @private + * @param {Number} textTopOffset Text top offset + * @param {Number} textLeftOffset Text left offset + * @return {Object} + */ + _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { + var textSpans = [], + textBgRects = [], + height = textTopOffset, lineOffset; + // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineOffset = this._getLineLeftOffset(i); + if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { + this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); + } + this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height); + height += this.getHeightOfLine(i); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, left, top) { + var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex), + styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace), + fillStyles = styleProps ? 'style="' + styleProps + '"' : '', + dy = styleDecl.deltaY, dySpan = '', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + if (dy) { + dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" '; + } + return [ + '', + fabric.util.string.escapeXml(_char), + '' + ].join(''); + }, + + _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, style, + boxWidth = 0, + line = this._textLines[lineIndex], + timeToRender; + + textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight; + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + textLeftOffset += charBox.kernedWidth - charBox.width; + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle); + } + if (timeToRender) { + style = this._getStyleDeclaration(lineIndex, i) || { }; + textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); + charsToRender = ''; + actualStyle = nextStyle; + textLeftOffset += boxWidth; + boxWidth = 0; + } + } + }, + + _pushTextBgRect: function(textBgRects, color, left, top, width, height) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + }, + + _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) { + var line = this._textLines[i], + heightOfLine = this.getHeightOfLine(i) / this.lineHeight, + boxWidth = 0, + boxStart = 0, + charBox, currentColor, + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (currentColor !== lastColor) { + lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart, + textTopOffset, boxWidth, heightOfLine); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart, + textTopOffset, boxWidth, heightOfLine); + }, + + /** + * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + * + * @private + * @param {*} value + * @return {String} + */ + _getFillAttributes: function(value) { + var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0, lastHeight = 0; + for (var j = 0; j < lineIndex; j++) { + lineTopOffset += this.getHeightOfLine(j); + } + lastHeight = this.getHeightOfLine(j); + return { + lineTop: lineTopOffset, + offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) + }; + }, + + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow); + return svgStyle + ' white-space: pre;'; + }, + }); +})(); +/* _TO_SVG_END_ */ + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = {}); + + /** + * Textbox class, based on IText, allows the user to resize the text rectangle + * and wraps lines automatically. Textboxes have their Y scaling locked, the + * user can only change width. Height is adjusted automatically based on the + * wrapping of lines. + * @class fabric.Textbox + * @extends fabric.IText + * @mixes fabric.Observable + * @return {fabric.Textbox} thisArg + * @see {@link fabric.Textbox#initialize} for constructor definition + */ + fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, { + + /** + * Type of an object + * @type String + * @default + */ + type: 'textbox', + + /** + * Minimum width of textbox, in pixels. + * @type Number + * @default + */ + minWidth: 20, + + /** + * Minimum calculated width of a textbox, in pixels. + * fixed to 2 so that an empty textbox cannot go to 0 + * and is still selectable without text. + * @type Number + * @default + */ + dynamicMinWidth: 2, + + /** + * Cached array of text wrapping. + * @type Array + */ + __cachedLines: null, + + /** + * Override standard Object class values + */ + lockScalingFlip: true, + + /** + * Override standard Object class values + * Textbox needs this on false + */ + noScaleCache: false, + + /** + * Properties which when set cause object to change dimensions + * @type Object + * @private + */ + _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'), + + /** + * Use this regular expression to split strings in breakable lines + * @private + */ + _wordJoiners: /[ \t\r]/, + + /** + * Use this boolean property in order to split strings that have no white space concept. + * this is a cheap way to help with chinese/japanese + * @type Boolean + * @since 2.6.0 + */ + splitByGrapheme: false, + + /** + * Unlike superclass's version of this function, Textbox does not update + * its width. + * @private + * @override + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this._clearCache(); + // clear dynamicMinWidth as it will be different after we re-wrap line + this.dynamicMinWidth = 0; + // wrap lines + this._styleMap = this._generateStyleMap(this._splitText()); + // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap + if (this.dynamicMinWidth > this.width) { + this._set('width', this.dynamicMinWidth); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + // clear cache and re-calculate height + this.height = this.calcTextHeight(); + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Generate an object that translates the style object so that it is + * broken up by visual lines (new lines and automatic wrapping). + * The original text styles object is broken up by actual lines (new lines only), + * which is only sufficient for Text / IText + * @private + */ + _generateStyleMap: function(textInfo) { + var realLineCount = 0, + realLineCharCount = 0, + charCount = 0, + map = {}; + + for (var i = 0; i < textInfo.graphemeLines.length; i++) { + if (textInfo.graphemeText[charCount] === '\n' && i > 0) { + realLineCharCount = 0; + charCount++; + realLineCount++; + } + else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) { + // this case deals with space's that are removed from end of lines when wrapping + realLineCharCount++; + charCount++; + } + + map[i] = { line: realLineCount, offset: realLineCharCount }; + + charCount += textInfo.graphemeLines[i].length; + realLineCharCount += textInfo.graphemeLines[i].length; + } + + return map; + }, + + /** + * Returns true if object has a style property or has it on a specified line + * @param {Number} lineIndex + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (map) { + lineIndex = map.line; + } + } + return fabric.Text.prototype.styleHas.call(this, property, lineIndex); + }, + + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false, + map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1]; + if (map) { + lineIndex = map.line; + offset = map.offset; + } + if (mapNextLine) { + nextLineIndex = mapNextLine.line; + shouldLimit = nextLineIndex === lineIndex; + nextOffset = mapNextLine.offset; + } + obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + } + return true; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (!map) { + return null; + } + lineIndex = map.line; + charIndex = map.offset + charIndex; + } + return this.callSuper('_getStyleDeclaration', lineIndex, charIndex); + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + this.styles[lineIndex][charIndex] = style; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + delete this.styles[lineIndex][charIndex]; + }, + + /** + * probably broken need a fix + * Returns the real style line that correspond to the wrapped lineIndex line + * Used just to verify if the line does exist or not. + * @param {Number} lineIndex + * @returns {Boolean} if the line exists or not + * @private + */ + _getLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + return !!this.styles[map.line]; + }, + + /** + * Set the line style to an empty object so that is initialized + * @param {Number} lineIndex + * @param {Object} style + * @private + */ + _setLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + this.styles[map.line] = {}; + }, + + /** + * Wraps text using the 'width' property of Textbox. First this function + * splits text on newlines, so we preserve newlines entered by the user. + * Then it wraps each line using the width of the Textbox by calling + * _wrapLine(). + * @param {Array} lines The string array of text that is split into lines + * @param {Number} desiredWidth width you want to wrap to + * @returns {Array} Array of lines + */ + _wrapText: function(lines, desiredWidth) { + var wrapped = [], i; + this.isWrapping = true; + for (i = 0; i < lines.length; i++) { + wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); + } + this.isWrapping = false; + return wrapped; + }, + + /** + * Helper function to measure a string of text, given its lineIndex and charIndex offset + * it gets called when charBounds are not available yet. + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {number} lineIndex + * @param {number} charOffset + * @returns {number} + * @private + */ + _measureWord: function(word, lineIndex, charOffset) { + var width = 0, prevGrapheme, skipLeft = true; + charOffset = charOffset || 0; + for (var i = 0, len = word.length; i < len; i++) { + var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft); + width += box.kernedWidth; + prevGrapheme = word[i]; + } + return width; + }, + + /** + * Wraps a line of text using the width of the Textbox and a context. + * @param {Array} line The grapheme array that represent the line + * @param {Number} lineIndex + * @param {Number} desiredWidth width you want to wrap the line to + * @param {Number} reservedSpace space to remove from wrapping for custom functionalities + * @returns {Array} Array of line(s) into which the given text is wrapped + * to. + */ + _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) { + var lineWidth = 0, + splitByGrapheme = this.splitByGrapheme, + graphemeLines = [], + line = [], + // spaces in different languages? + words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), + word = '', + offset = 0, + infix = splitByGrapheme ? '' : ' ', + wordWidth = 0, + infixWidth = 0, + largestWordWidth = 0, + lineJustStarted = true, + additionalSpace = this._getWidthOfCharSpacing(), + reservedSpace = reservedSpace || 0; + // fix a difference between split and graphemeSplit + if (words.length === 0) { + words.push([]); + } + desiredWidth -= reservedSpace; + for (var i = 0; i < words.length; i++) { + // if using splitByGrapheme words are already in graphemes. + word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); + wordWidth = this._measureWord(word, lineIndex, offset); + offset += word.length; + + lineWidth += infixWidth + wordWidth - additionalSpace; + if (lineWidth > desiredWidth && !lineJustStarted) { + graphemeLines.push(line); + line = []; + lineWidth = wordWidth; + lineJustStarted = true; + } + else { + lineWidth += additionalSpace; + } + + if (!lineJustStarted && !splitByGrapheme) { + line.push(infix); + } + line = line.concat(word); + + infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); + offset++; + lineJustStarted = false; + // keep track of largest word + if (wordWidth > largestWordWidth) { + largestWordWidth = wordWidth; + } + } + + i && graphemeLines.push(line); + + if (largestWordWidth + reservedSpace > this.dynamicMinWidth) { + this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace; + } + return graphemeLines; + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @param {Number} lineIndex text to split + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + if (!this._styleMap[lineIndex + 1]) { + // is last line, return true; + return true; + } + if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) { + // this is last line before a line break, return true; + return true; + } + return false; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * @return Number + */ + missingNewlineOffset: function(lineIndex) { + if (this.splitByGrapheme) { + return this.isEndOfWrapping(lineIndex) ? 1 : 0; + } + return 1; + }, + + /** + * Gets lines of text to render in the Textbox. This function calculates + * text wrapping on the fly every time it is called. + * @param {String} text text to split + * @returns {Array} Array of lines in the Textbox. + * @override + */ + _splitTextIntoLines: function(text) { + var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text), + graphemeLines = this._wrapText(newText.lines, this.width), + lines = new Array(graphemeLines.length); + for (var i = 0; i < graphemeLines.length; i++) { + lines[i] = graphemeLines[i].join(''); + } + newText.lines = lines; + newText.graphemeLines = graphemeLines; + return newText; + }, + + getMinWidth: function() { + return Math.max(this.minWidth, this.dynamicMinWidth); + }, + + _removeExtraneousStyles: function() { + var linesToKeep = {}; + for (var prop in this._styleMap) { + if (this._textLines[prop]) { + linesToKeep[this._styleMap[prop].line] = 1; + } + } + for (var prop in this.styles) { + if (!linesToKeep[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)); + } + }); + + /** + * Returns fabric.Textbox instance from an object representation + * @static + * @memberOf fabric.Textbox + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created + */ + fabric.Textbox.fromObject = function(object, callback) { + return fabric.Object._fromObject('Textbox', object, callback, 'text'); + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var controlsUtils = fabric.controlsUtils, + scaleSkewStyleHandler = controlsUtils.scaleSkewCursorStyleHandler, + scaleStyleHandler = controlsUtils.scaleCursorStyleHandler, + scalingEqually = controlsUtils.scalingEqually, + scalingYOrSkewingX = controlsUtils.scalingYOrSkewingX, + scalingXOrSkewingY = controlsUtils.scalingXOrSkewingY, + scaleOrSkewActionName = controlsUtils.scaleOrSkewActionName, + objectControls = fabric.Object.prototype.controls; + + objectControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mb = new fabric.Control({ + x: 0, + y: 0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mt = new fabric.Control({ + x: 0, + y: -0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); + + objectControls.tl = new fabric.Control({ + x: -0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.tr = new fabric.Control({ + x: 0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.bl = new fabric.Control({ + x: -0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.br = new fabric.Control({ + x: 0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.mtr = new fabric.Control({ + x: 0, + y: -0.5, + actionHandler: controlsUtils.rotationWithSnapping, + cursorStyleHandler: controlsUtils.rotationStyleHandler, + offsetY: -120, + withConnection: true, + actionName: 'rotate', + }); + + if (fabric.Textbox) { + // this is breaking the prototype inheritance, no time / ideas to fix it. + // is important to document that if you want to have all objects to have a + // specific custom control, you have to add it to Object prototype and to Textbox + // prototype. The controls are shared as references. So changes to control `tr` + // can still apply to all objects if needed. + var textBoxControls = fabric.Textbox.prototype.controls = { }; + + textBoxControls.mtr = objectControls.mtr; + textBoxControls.tr = objectControls.tr; + textBoxControls.br = objectControls.br; + textBoxControls.tl = objectControls.tl; + textBoxControls.bl = objectControls.bl; + textBoxControls.mt = objectControls.mt; + textBoxControls.mb = objectControls.mb; + + textBoxControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', + }); + + textBoxControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', + }); + } +})(); + diff --git a/frontend-web/vendor/js/fabric2.min.js b/frontend-web/vendor/js/fabric2.min.js new file mode 100644 index 0000000..f85f187 --- /dev/null +++ b/frontend-web/vendor/js/fabric2.min.js @@ -0,0 +1,30786 @@ +/* build: `node build.js modules=ALL exclude=gestures,accessors,erasing requirejs minifier=uglifyjs` */ +/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: '4.6.0' }; +if (typeof exports !== 'undefined') { + exports.fabric = fabric; +} +/* _AMD_START_ */ +else if (typeof define === 'function' && define.amd) { + define([], function() { return fabric; }); +} +/* _AMD_END_ */ +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + if (document instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document)) { + fabric.document = document; + } + else { + fabric.document = document.implementation.createHTMLDocument(''); + } + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + var jsdom = require('jsdom'); + var virtualWindow = new jsdom.JSDOM( + decodeURIComponent('%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E'), + { + features: { + FetchExternalResources: ['img'] + }, + resources: 'usable' + }).window; + fabric.document = virtualWindow.document; + fabric.jsdomImplForWrapper = require('jsdom/lib/jsdom/living/generated/utils').implForWrapper; + fabric.nodeCanvas = require('jsdom/lib/jsdom/utils').Canvas; + fabric.window = virtualWindow; + DOMParser = fabric.window.DOMParser; +} + +/** + * True when in environment that supports touch events + * @type boolean + */ +fabric.isTouchSupported = 'ontouchstart' in fabric.window || 'ontouchstart' in fabric.document || + (fabric.window && fabric.window.navigator && fabric.window.navigator.maxTouchPoints > 0); + +/** + * True when in environment that's probably Node.js + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && + typeof window === 'undefined'; + +/* _FROM_SVG_START_ */ +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + 'display', + 'transform', + 'fill', 'fill-opacity', 'fill-rule', + 'opacity', + 'stroke', 'stroke-dasharray', 'stroke-linecap', 'stroke-dashoffset', + 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-opacity', 'stroke-width', + 'id', 'paint-order', 'vector-effect', + 'instantiated_by_use', 'clip-path', +]; +/* _FROM_SVG_END_ */ + +/** + * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. + */ +fabric.DPI = 96; +fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)'; +fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'; +fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig; +fabric.reNonWord = /[ \n\.,;!\?\-]/; +fabric.fontPaths = { }; +fabric.iMatrix = [1, 0, 0, 1, 0, 0]; +fabric.svgNS = 'http://www.w3.org/2000/svg'; + +/** + * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine. + * @since 1.7.14 + * @type Number + * @default + */ +fabric.perfLimitSizeTotal = 2097152; + +/** + * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000 + * @since 1.7.14 + * @type Number + * @default + */ +fabric.maxCacheSideLimit = 4096; + +/** + * Lowest pixel limit for cache canvases, set at 256PX + * @since 1.7.14 + * @type Number + * @default + */ +fabric.minCacheSideLimit = 256; + +/** + * Cache Object for widths of chars in text rendering. + */ +fabric.charWidthsCache = { }; + +/** + * if webgl is enabled and available, textureSize will determine the size + * of the canvas backend + * @since 2.0.0 + * @type Number + * @default + */ +fabric.textureSize = 2048; + +/** + * When 'true', style information is not retained when copy/pasting text, making + * pasted text use destination style. + * Defaults to 'false'. + * @type Boolean + * @default + */ +fabric.disableStyleCopyPaste = false; + +/** + * Enable webgl for filtering picture is available + * A filtering backend will be initialized, this will both take memory and + * time since a default 2048x2048 canvas will be created for the gl context + * @since 2.0.0 + * @type Boolean + * @default + */ +fabric.enableGLFiltering = true; + +/** + * Device Pixel Ratio + * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html + */ +fabric.devicePixelRatio = fabric.window.devicePixelRatio || + fabric.window.webkitDevicePixelRatio || + fabric.window.mozDevicePixelRatio || + 1; +/** + * Browser-specific constant to adjust CanvasRenderingContext2D.shadowBlur value, + * which is unitless and not rendered equally across browsers. + * + * Values that work quite well (as of October 2017) are: + * - Chrome: 1.5 + * - Edge: 1.75 + * - Firefox: 0.9 + * - Safari: 0.95 + * + * @since 2.0.0 + * @type Number + * @default 1 + */ +fabric.browserShadowBlurConstant = 1; + +/** + * This object contains the result of arc to bezier conversion for faster retrieving if the same arc needs to be converted again. + * It was an internal variable, is accessible since version 2.3.4 + */ +fabric.arcToSegmentsCache = { }; + +/** + * This object keeps the results of the boundsOfCurve calculation mapped by the joined arguments necessary to calculate it. + * It does speed up calculation, if you parse and add always the same paths, but in case of heavy usage of freedrawing + * you do not get any speed benefit and you get a big object in memory. + * The object was a private variable before, while now is appended to the lib so that you have access to it and you + * can eventually clear it. + * It was an internal variable, is accessible since version 2.3.4 + */ +fabric.boundsOfCurveCache = { }; + +/** + * If disabled boundsOfCurveCache is not used. For apps that make heavy usage of pencil drawing probably disabling it is better + * @default true + */ +fabric.cachesBoundsOfCurve = true; + +/** + * Skip performance testing of setupGLContext and force the use of putImageData that seems to be the one that works best on + * Chrome + old hardware. if your users are experiencing empty images after filtering you may try to force this to true + * this has to be set before instantiating the filtering backend ( before filtering the first image ) + * @type Boolean + * @default false + */ +fabric.forceGLPutImageData = false; + +fabric.initFilterBackend = function() { + if (fabric.enableGLFiltering && fabric.isWebglSupported && fabric.isWebglSupported(fabric.textureSize)) { + console.log('max texture size: ' + fabric.maxTextureSize); + return (new fabric.WebglFilterBackend({ tileSize: fabric.textureSize })); + } + else if (fabric.Canvas2dFilterBackend) { + return (new fabric.Canvas2dFilterBackend()); + } +}; + + +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) + window.fabric = fabric; +} + + +(function() { + + /** + * @private + * @param {String} eventName + * @param {Function} handler + */ + function _removeEventListener(eventName, handler) { + if (!this.__eventListeners[eventName]) { + return; + } + var eventListener = this.__eventListeners[eventName]; + if (handler) { + eventListener[eventListener.indexOf(handler)] = false; + } + else { + fabric.util.array.fill(eventListener, false); + } + } + + /** + * Observes specified event + * @memberOf fabric.Observable + * @alias on + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function that receives a notification when an event of the specified type occurs + * @return {Self} thisArg + * @chainable + */ + function on(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = []; + } + this.__eventListeners[eventName].push(handler); + } + return this; + } + + function _once(eventName, handler) { + var _handler = function () { + handler.apply(this, arguments); + this.off(eventName, _handler); + }.bind(this); + this.on(eventName, _handler); + } + + function once(eventName, handler) { + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + _once.call(this, prop, eventName[prop]); + } + } + else { + _once.call(this, eventName, handler); + } + return this; + } + + /** + * Stops event observing for a particular event handler. Calling this method + * without arguments removes all handlers for all events + * @memberOf fabric.Observable + * @alias off + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function to be deleted from EventListeners + * @return {Self} thisArg + * @chainable + */ + function off(eventName, handler) { + if (!this.__eventListeners) { + return this; + } + + // remove all key/value pairs (event name -> event handler) + if (arguments.length === 0) { + for (eventName in this.__eventListeners) { + _removeEventListener.call(this, eventName); + } + } + // one object with key/value pairs was passed + else if (arguments.length === 1 && typeof arguments[0] === 'object') { + for (var prop in eventName) { + _removeEventListener.call(this, prop, eventName[prop]); + } + } + else { + _removeEventListener.call(this, eventName, handler); + } + return this; + } + + /** + * Fires event with an optional options object + * @memberOf fabric.Observable + * @param {String} eventName Event name to fire + * @param {Object} [options] Options object + * @return {Self} thisArg + * @chainable + */ + function fire(eventName, options) { + if (!this.__eventListeners) { + return this; + } + + var listenersForEvent = this.__eventListeners[eventName]; + if (!listenersForEvent) { + return this; + } + + for (var i = 0, len = listenersForEvent.length; i < len; i++) { + listenersForEvent[i] && listenersForEvent[i].call(this, options || { }); + } + this.__eventListeners[eventName] = listenersForEvent.filter(function(value) { + return value !== false; + }); + return this; + } + + /** + * @namespace fabric.Observable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events} + * @see {@link http://fabricjs.com/events|Events demo} + */ + fabric.Observable = { + fire: fire, + on: on, + once: once, + off: off, + }; +})(); + + +/** + * @namespace fabric.Collection + */ +fabric.Collection = { + + _objects: [], + + /** + * Adds objects to collection, Canvas or Group, then renders canvas + * (if `renderOnAddRemove` is not `false`). + * in case of Group no changes to bounding box are made. + * Objects should be instances of (or inherit from) fabric.Object + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the add method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + if (this._onObjectAdded) { + for (var i = 0, length = arguments.length; i < length; i++) { + this._onObjectAdded(arguments[i]); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) + * An object should be an instance of (or inherit from) fabric.Object + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the insertAt method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {Object} object Object to insert + * @param {Number} index Index to insert object at + * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs + * @return {Self} thisArg + * @chainable + */ + insertAt: function (object, index, nonSplicing) { + var objects = this._objects; + if (nonSplicing) { + objects[index] = object; + } + else { + objects.splice(index, 0, object); + } + this._onObjectAdded && this._onObjectAdded(object); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + remove: function() { + var objects = this._objects, + index, somethingRemoved = false; + + for (var i = 0, length = arguments.length; i < length; i++) { + index = objects.indexOf(arguments[i]); + + // only call onObjectRemoved if an object was actually removed + if (index !== -1) { + somethingRemoved = true; + objects.splice(index, 1); + this._onObjectRemoved && this._onObjectRemoved(arguments[i]); + } + } + + this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); + return this; + }, + + /** + * Executes given function for each object in this group + * @param {Function} callback + * Callback invoked with current object as first argument, + * index - as second and an array of all objects - as third. + * Callback is invoked in a context of Global Object (e.g. `window`) + * when no `context` argument is given + * + * @param {Object} context Context (aka thisObject) + * @return {Self} thisArg + * @chainable + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(); + for (var i = 0, len = objects.length; i < len; i++) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Returns an array of children objects of this instance + * Type parameter introduced in 1.3.10 + * since 2.3.5 this method return always a COPY of the array; + * @param {String} [type] When specified, only objects of this type are returned + * @return {Array} + */ + getObjects: function(type) { + if (typeof type === 'undefined') { + return this._objects.concat(); + } + return this._objects.filter(function(o) { + return o.type === type; + }); + }, + + /** + * Returns object at specified index + * @param {Number} index + * @return {Self} thisArg + */ + item: function (index) { + return this._objects[index]; + }, + + /** + * Returns true if collection contains no objects + * @return {Boolean} true if collection is empty + */ + isEmpty: function () { + return this._objects.length === 0; + }, + + /** + * Returns a size of a collection (i.e: length of an array containing its objects) + * @return {Number} Collection size + */ + size: function() { + return this._objects.length; + }, + + /** + * Returns true if collection contains an object + * @param {Object} object Object to check against + * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` + * @return {Boolean} `true` if collection contains an object + */ + contains: function (object, deep) { + if (this._objects.indexOf(object) > -1) { + return true; + } + else if (deep) { + return this._objects.some(function (obj) { + return typeof obj.contains === 'function' && obj.contains(object, true); + }); + } + return false; + }, + + /** + * Returns number representation of a collection complexity + * @return {Number} complexity + */ + complexity: function () { + return this._objects.reduce(function (memo, current) { + memo += current.complexity ? current.complexity() : 0; + return memo; + }, 0); + } +}; + + +/** + * @namespace fabric.CommonMethods + */ +fabric.CommonMethods = { + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + _setOptions: function(options) { + for (var prop in options) { + this.set(prop, options[prop]); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Gradient to + */ + _initGradient: function(filler, property) { + if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { + this.set(property, new fabric.Gradient(filler)); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Pattern to + * @param {Function} [callback] callback to invoke after pattern load + */ + _initPattern: function(filler, property, callback) { + if (filler && filler.source && !(filler instanceof fabric.Pattern)) { + this.set(property, new fabric.Pattern(filler, callback)); + } + else { + callback && callback(); + } + }, + + /** + * @private + */ + _setObject: function(obj) { + for (var prop in obj) { + this._set(prop, obj[prop]); + } + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + this._setObject(key); + } + else { + this._set(key, value); + } + return this; + }, + + _set: function(key, value) { + this[key] = value; + }, + + /** + * Toggles specified property from `true` to `false` or from `false` to `true` + * @param {String} property Property to toggle + * @return {fabric.Object} thisArg + * @chainable + */ + toggle: function(property) { + var value = this.get(property); + if (typeof value === 'boolean') { + this.set(property, !value); + } + return this; + }, + + /** + * Basic getter + * @param {String} property Property name + * @return {*} value of a property + */ + get: function(property) { + return this[property]; + } +}; + + +(function(global) { + + var sqrt = Math.sqrt, + atan2 = Math.atan2, + pow = Math.pow, + PiBy180 = Math.PI / 180, + PiBy2 = Math.PI / 2; + + /** + * @namespace fabric.util + */ + fabric.util = { + + /** + * Calculate the cos of an angle, avoiding returning floats for known results + * @static + * @memberOf fabric.util + * @param {Number} angle the angle in radians or in degree + * @return {Number} + */ + cos: function(angle) { + if (angle === 0) { return 1; } + if (angle < 0) { + // cos(a) = cos(-a) + angle = -angle; + } + var angleSlice = angle / PiBy2; + switch (angleSlice) { + case 1: case 3: return 0; + case 2: return -1; + } + return Math.cos(angle); + }, + + /** + * Calculate the sin of an angle, avoiding returning floats for known results + * @static + * @memberOf fabric.util + * @param {Number} angle the angle in radians or in degree + * @return {Number} + */ + sin: function(angle) { + if (angle === 0) { return 0; } + var angleSlice = angle / PiBy2, sign = 1; + if (angle < 0) { + // sin(-a) = -sin(a) + sign = -1; + } + switch (angleSlice) { + case 1: return sign; + case 2: return 0; + case 3: return -sign; + } + return Math.sin(angle); + }, + + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @param {Array} array + * @param {*} value + * @return {Array} original array + */ + removeFromArray: function(array, value) { + var idx = array.indexOf(value); + if (idx !== -1) { + array.splice(idx, 1); + } + return array; + }, + + /** + * Returns random number between 2 specified ones. + * @static + * @memberOf fabric.util + * @param {Number} min lower limit + * @param {Number} max upper limit + * @return {Number} random value (between min and max) + */ + getRandomInt: function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, + + /** + * Transforms degrees to radians. + * @static + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + degreesToRadians: function(degrees) { + return degrees * PiBy180; + }, + + /** + * Transforms radians to degrees. + * @static + * @memberOf fabric.util + * @param {Number} radians value in radians + * @return {Number} value in degrees + */ + radiansToDegrees: function(radians) { + return radians / PiBy180; + }, + + /** + * Rotates `point` around `origin` with `radians` + * @static + * @memberOf fabric.util + * @param {fabric.Point} point The point to rotate + * @param {fabric.Point} origin The origin of the rotation + * @param {Number} radians The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), + v = fabric.util.rotateVector(newPoint, radians); + return new fabric.Point(v.x, v.y).addEquals(origin); + }, + + /** + * Rotates `vector` with `radians` + * @static + * @memberOf fabric.util + * @param {Object} vector The vector to rotate (x and y) + * @param {Number} radians The radians of the angle for the rotation + * @return {Object} The new rotated point + */ + rotateVector: function(vector, radians) { + var sin = fabric.util.sin(radians), + cos = fabric.util.cos(radians), + rx = vector.x * cos - vector.y * sin, + ry = vector.x * sin + vector.y * cos; + return { + x: rx, + y: ry + }; + }, + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[2] * p.y, + t[1] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[2] * p.y + t[4], + t[1] * p.x + t[3] * p.y + t[5] + ); + }, + + /** + * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @param {Array} points 4 points array + * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix + * @return {Object} Object with left, top, width, height properties + */ + makeBoundingBoxFromPoints: function(points, transform) { + if (transform) { + for (var i = 0; i < points.length; i++) { + points[i] = fabric.util.transformPoint(points[i], transform); + } + } + var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], + minX = fabric.util.array.min(xPoints), + maxX = fabric.util.array.max(xPoints), + width = maxX - minX, + yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], + minY = fabric.util.array.min(yPoints), + maxY = fabric.util.array.max(yPoints), + height = maxY - minY; + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var a = 1 / (t[0] * t[3] - t[1] * t[2]), + r = [a * t[3], -a * t[1], -a * t[2], a * t[0]], + o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true); + r[4] = -o.x; + r[5] = -o.y; + return r; + }, + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @memberOf fabric.util + * @param {Number|String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, + + /** + * Converts from attribute value to pixel value if applicable. + * Returns converted pixels or original value not converted. + * @param {Number|String} value number to operate on + * @param {Number} fontSize + * @return {Number|String} + */ + parseUnit: function(value, fontSize) { + var unit = /\D{0,2}$/.exec(value), + number = parseFloat(value); + if (!fontSize) { + fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + switch (unit[0]) { + case 'mm': + return number * fabric.DPI / 25.4; + + case 'cm': + return number * fabric.DPI / 2.54; + + case 'in': + return number * fabric.DPI; + + case 'pt': + return number * fabric.DPI / 72; // or * 4 / 3 + + case 'pc': + return number * fabric.DPI / 72 * 12; // or * 16 + + case 'em': + return number * fontSize; + + default: + return number; + } + }, + + /** + * Function which always returns `false`. + * @static + * @memberOf fabric.util + * @return {Boolean} + */ + falseFunction: function() { + return false; + }, + + /** + * Returns klass "Class" object of given namespace + * @memberOf fabric.util + * @param {String} type Type of object (eg. 'circle') + * @param {String} namespace Namespace to get klass "Class" object from + * @return {Object} klass "Class" + */ + getKlass: function(type, namespace) { + // capitalize first letter only + type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); + return fabric.util.resolveNamespace(namespace)[type]; + }, + + /** + * Returns array of attributes for given svg that fabric parses + * @memberOf fabric.util + * @param {String} type Type of svg element (eg. 'circle') + * @return {Array} string names of supported attributes + */ + getSvgAttributes: function(type) { + var attributes = [ + 'instantiated_by_use', + 'style', + 'id', + 'class' + ]; + switch (type) { + case 'linearGradient': + attributes = attributes.concat(['x1', 'y1', 'x2', 'y2', 'gradientUnits', 'gradientTransform']); + break; + case 'radialGradient': + attributes = attributes.concat(['gradientUnits', 'gradientTransform', 'cx', 'cy', 'r', 'fx', 'fy', 'fr']); + break; + case 'stop': + attributes = attributes.concat(['offset', 'stop-color', 'stop-opacity']); + break; + } + return attributes; + }, + + /** + * Returns object of given namespace + * @memberOf fabric.util + * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' + * @return {Object} Object for given namespace (default fabric) + */ + resolveNamespace: function(namespace) { + if (!namespace) { + return fabric; + } + + var parts = namespace.split('.'), + len = parts.length, i, + obj = global || fabric.window; + + for (i = 0; i < len; ++i) { + obj = obj[parts[i]]; + } + + return obj; + }, + + /** + * Loads image element from given url and passes it to a callback + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {*} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } + + var img = fabric.util.createImage(); + + /** @ignore */ + var onLoadCallback = function () { + callback && callback.call(context, img, false); + img = img.onload = img.onerror = null; + }; + + img.onload = onLoadCallback; + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + // crossOrigin null is the same as not set. + if (url.indexOf('data') !== 0 && + crossOrigin !== undefined && + crossOrigin !== null) { + img.crossOrigin = crossOrigin; + } + + // IE10 / IE11-Fix: SVG contents from data: URI + // will only be available if the IMG is present + // in the DOM (and visible) + if (url.substring(0,14) === 'data:image/svg') { + img.onload = null; + fabric.util.loadImageInDom(img, onLoadCallback); + } + + img.src = url; + }, + + /** + * Attaches SVG image with data: URL to the dom + * @memberOf fabric.util + * @param {Object} img Image object with data:image/svg src + * @param {Function} callback Callback; invoked with loaded image + * @return {Object} DOM element (div containing the SVG image) + */ + loadImageInDom: function(img, onLoadCallback) { + var div = fabric.document.createElement('div'); + div.style.width = div.style.height = '1px'; + div.style.left = div.style.top = '-100%'; + div.style.position = 'absolute'; + div.appendChild(img); + fabric.document.querySelector('body').appendChild(div); + /** + * Wrap in function to: + * 1. Call existing callback + * 2. Cleanup DOM + */ + img.onload = function () { + onLoadCallback(); + div.parentNode.removeChild(div); + div = null; + }; + }, + + /** + * Creates corresponding fabric instances from their object representations + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {String} namespace Namespace to get klass "Class" object from + * @param {Function} reviver Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || []; + + var enlivenedObjects = [], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects.filter(function(obj) { + // filter out undefined objects (objects that gave error) + return obj; + })); + } + } + + if (!numTotalObjects) { + callback && callback(enlivenedObjects); + return; + } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { + onLoaded(); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + klass.fromObject(o, function (obj, error) { + error || (enlivenedObjects[index] = obj); + reviver && reviver(o, obj, error); + onLoaded(); + }); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} patterns Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * called after each fabric object created. + */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; + + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); + } + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; + onLoaded(); + }); + } + else { + enlivenedPatterns[index] = p; + onLoaded(); + } + }); + }, + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @param {String} path Value to set sourcePath to + * @return {fabric.Object|fabric.Group} + */ + groupSVGElements: function(elements, options, path) { + var object; + if (elements && elements.length === 1) { + return elements[0]; + } + if (options) { + if (options.width && options.height) { + options.centerPoint = { + x: options.width / 2, + y: options.height / 2 + }; + } + else { + delete options.width; + delete options.height; + } + } + object = new fabric.Group(elements, options); + if (typeof path !== 'undefined') { + object.sourcePath = path; + } + return object; + }, + + /** + * Populates an object with properties of another object + * @static + * @memberOf fabric.util + * @param {Object} source Source object + * @param {Object} destination Destination object + * @return {Array} properties Properties names to include + */ + populateWithProperties: function(source, destination, properties) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + for (var i = 0, len = properties.length; i < len; i++) { + if (properties[i] in source) { + destination[properties[i]] = source[properties[i]]; + } + } + } + }, + + /** + * WARNING: THIS WAS TO SUPPORT OLD BROWSERS. deprecated. + * WILL BE REMOVED IN FABRIC 5.0 + * Draws a dashed line between two points + * + * This method is used to draw dashed line around selection area. + * See dotted stroke in canvas + * + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x start x coordinate + * @param {Number} y start y coordinate + * @param {Number} x2 end x coordinate + * @param {Number} y2 end y coordinate + * @param {Array} da dash array pattern + * @deprecated + */ + drawDashedLine: function(ctx, x, y, x2, y2, da) { + var dx = x2 - x, + dy = y2 - y, + len = sqrt(dx * dx + dy * dy), + rot = atan2(dy, dx), + dc = da.length, + di = 0, + draw = true; + + ctx.save(); + ctx.translate(x, y); + ctx.moveTo(0, 0); + ctx.rotate(rot); + + x = 0; + while (len > x) { + x += da[di++ % dc]; + if (x > len) { + x = len; + } + ctx[draw ? 'lineTo' : 'moveTo'](x, 0); + draw = !draw; + } + + ctx.restore(); + }, + + /** + * Creates canvas element + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element + */ + createCanvasElement: function() { + return fabric.document.createElement('canvas'); + }, + + /** + * Creates a canvas element that is a copy of another and is also painted + * @param {CanvasElement} canvas to copy size and content of + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element + */ + copyCanvasElement: function(canvas) { + var newCanvas = fabric.util.createCanvasElement(); + newCanvas.width = canvas.width; + newCanvas.height = canvas.height; + newCanvas.getContext('2d').drawImage(canvas, 0, 0); + return newCanvas; + }, + + /** + * since 2.6.0 moved from canvas instance to utility. + * @param {CanvasElement} canvasEl to copy size and content of + * @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too + * @param {Number} quality <= 1 and > 0 + * @static + * @memberOf fabric.util + * @return {String} data url + */ + toDataURL: function(canvasEl, format, quality) { + return canvasEl.toDataURL('image/' + format, quality); + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + createImage: function() { + return fabric.document.createElement('img'); + }, + + /** + * Multiply matrix A by matrix B to nest transformations + * @static + * @memberOf fabric.util + * @param {Array} a First transformMatrix + * @param {Array} b Second transformMatrix + * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(a, b, is2x2) { + // Matrix multiply a * b + return [ + a[0] * b[0] + a[2] * b[1], + a[1] * b[0] + a[3] * b[1], + a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], + is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], + is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] + ]; + }, + + /** + * Decomposes standard 2x3 matrix into transform components + * @static + * @memberOf fabric.util + * @param {Array} a transformMatrix + * @return {Object} Components of transform + */ + qrDecompose: function(a) { + var angle = atan2(a[1], a[0]), + denom = pow(a[0], 2) + pow(a[1], 2), + scaleX = sqrt(denom), + scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX, + skewX = atan2(a[0] * a[2] + a[1] * a [3], denom); + return { + angle: angle / PiBy180, + scaleX: scaleX, + scaleY: scaleY, + skewX: skewX / PiBy180, + skewY: 0, + translateX: a[4], + translateY: a[5] + }; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] angle in degrees + * @return {Number[]} transform matrix + */ + calcRotateMatrix: function(options) { + if (!options.angle) { + return fabric.iMatrix.concat(); + } + var theta = fabric.util.degreesToRadians(options.angle), + cos = fabric.util.cos(theta), + sin = fabric.util.sin(theta); + return [cos, sin, -sin, cos, 0, 0]; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet. + * is called DimensionsTransformMatrix because those properties are the one that influence + * the size of the resulting box of the object. + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @return {Number[]} transform matrix + */ + calcDimensionsMatrix: function(options) { + var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, + scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, + scaleMatrix = [ + options.flipX ? -scaleX : scaleX, + 0, + 0, + options.flipY ? -scaleY : scaleY, + 0, + 0], + multiply = fabric.util.multiplyTransformMatrices, + degreesToRadians = fabric.util.degreesToRadians; + if (options.skewX) { + scaleMatrix = multiply( + scaleMatrix, + [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], + true); + } + if (options.skewY) { + scaleMatrix = multiply( + scaleMatrix, + [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], + true); + } + return scaleMatrix; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @param {Number} [options.translateX] + * @param {Number} [options.translateY] + * @return {Number[]} transform matrix + */ + composeMatrix: function(options) { + var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], + multiply = fabric.util.multiplyTransformMatrices; + if (options.angle) { + matrix = multiply(matrix, fabric.util.calcRotateMatrix(options)); + } + if (options.scaleX !== 1 || options.scaleY !== 1 || + options.skewX || options.skewY || options.flipX || options.flipY) { + matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options)); + } + return matrix; + }, + + /** + * reset an object transform state to neutral. Top and left are not accounted for + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to transform + */ + resetObjectTransform: function (target) { + target.scaleX = 1; + target.scaleY = 1; + target.skewX = 0; + target.skewY = 0; + target.flipX = false; + target.flipY = false; + target.rotate(0); + }, + + /** + * Extract Object transform values + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to read from + * @return {Object} Components of transform + */ + saveObjectTransform: function (target) { + return { + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + angle: target.angle, + left: target.left, + flipX: target.flipX, + flipY: target.flipY, + top: target.top + }; + }, + + /** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Number} tolerance Tolerance + */ + isTransparent: function(ctx, x, y, tolerance) { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + var _isTransparent = true, i, temp, + imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1), + l = imageData.data.length; + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (i = 3; i < l; i += 4) { + temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) { + break; // Stop if colour found + } + } + + imageData = null; + + return _isTransparent; + }, + + /** + * Parse preserveAspectRatio attribute from element + * @param {string} attribute to be parsed + * @return {Object} an object containing align and meetOrSlice attribute + */ + parsePreserveAspectRatioAttribute: function(attribute) { + var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid', + aspectRatioAttrs = attribute.split(' '), align; + + if (aspectRatioAttrs && aspectRatioAttrs.length) { + meetOrSlice = aspectRatioAttrs.pop(); + if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') { + align = meetOrSlice; + meetOrSlice = 'meet'; + } + else if (aspectRatioAttrs.length) { + align = aspectRatioAttrs.pop(); + } + } + //divide align in alignX and alignY + alignX = align !== 'none' ? align.slice(1, 4) : 'none'; + alignY = align !== 'none' ? align.slice(5, 8) : 'none'; + return { + meetOrSlice: meetOrSlice, + alignX: alignX, + alignY: alignY + }; + }, + + /** + * Clear char widths cache for the given font family or all the cache if no + * fontFamily is specified. + * Use it if you know you are loading fonts in a lazy way and you are not waiting + * for custom fonts to load properly when adding text objects to the canvas. + * If a text object is added when its own font is not loaded yet, you will get wrong + * measurement and so wrong bounding boxes. + * After the font cache is cleared, either change the textObject text content or call + * initDimensions() to trigger a recalculation + * @memberOf fabric.util + * @param {String} [fontFamily] font family to clear + */ + clearFabricFontCache: function(fontFamily) { + fontFamily = (fontFamily || '').toLowerCase(); + if (!fontFamily) { + fabric.charWidthsCache = { }; + } + else if (fabric.charWidthsCache[fontFamily]) { + delete fabric.charWidthsCache[fontFamily]; + } + }, + + /** + * Given current aspect ratio, determines the max width and height that can + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Number} ar aspect ratio + * @param {Number} maximumArea Maximum area you want to achieve + * @return {Object.x} Limited dimensions by X + * @return {Object.y} Limited dimensions by Y + */ + limitDimsByArea: function(ar, maximumArea) { + var roughWidth = Math.sqrt(maximumArea * ar), + perfLimitSizeY = Math.floor(maximumArea / roughWidth); + return { x: Math.floor(roughWidth), y: perfLimitSizeY }; + }, + + capValue: function(min, value, max) { + return Math.max(min, Math.min(value, max)); + }, + + /** + * Finds the scale for the object source to fit inside the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to fit into destination + */ + findScaleToFit: function(source, destination) { + return Math.min(destination.width / source.width, destination.height / source.height); + }, + + /** + * Finds the scale for the object source to cover entirely the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to cover destination + */ + findScaleToCover: function(source, destination) { + return Math.max(destination.width / source.width, destination.height / source.height); + }, + + /** + * given an array of 6 number returns something like `"matrix(...numbers)"` + * @memberOf fabric.util + * @param {Array} transform an array with 6 numbers + * @return {String} transform matrix for svg + * @return {Object.y} Limited dimensions by Y + */ + matrixToSVG: function(transform) { + return 'matrix(' + transform.map(function(value) { + return fabric.util.toFixed(value, fabric.Object.NUM_FRACTION_DIGITS); + }).join(' ') + ')'; + }, + + /** + * given an object and a transform, apply the inverse transform to the object, + * this is equivalent to remove from that object that transformation, so that + * added in a space with the removed transform, the object will be the same as before. + * Removing from an object a transform that scale by 2 is like scaling it by 1/2. + * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg + * in the opposite direction. + * This util is used to add objects inside transformed groups or nested groups. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + removeTransformFromObject: function(object, transform) { + var inverted = fabric.util.invertTransform(transform), + finalTransform = fabric.util.multiplyTransformMatrices(inverted, object.calcOwnMatrix()); + fabric.util.applyTransformToObject(object, finalTransform); + }, + + /** + * given an object and a transform, apply the transform to the object. + * this is equivalent to change the space where the object is drawn. + * Adding to an object a transform that scale by 2 is like scaling it by 2. + * This is used when removing an object from an active selection for example. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + addTransformToObject: function(object, transform) { + fabric.util.applyTransformToObject( + object, + fabric.util.multiplyTransformMatrices(transform, object.calcOwnMatrix()) + ); + }, + + /** + * discard an object transform state and apply the one from the matrix. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + applyTransformToObject: function(object, transform) { + var options = fabric.util.qrDecompose(transform), + center = new fabric.Point(options.translateX, options.translateY); + object.flipX = false; + object.flipY = false; + object.set('scaleX', options.scaleX); + object.set('scaleY', options.scaleY); + object.skewX = options.skewX; + object.skewY = options.skewY; + object.angle = options.angle; + object.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * given a width and height, return the size of the bounding box + * that can contains the box with width/height with applied transform + * described in options. + * Use to calculate the boxes around objects for controls. + * @memberOf fabric.util + * @param {Number} width + * @param {Number} height + * @param {Object} options + * @param {Number} options.scaleX + * @param {Number} options.scaleY + * @param {Number} options.skewX + * @param {Number} options.skewY + * @return {Object.x} width of containing + * @return {Object.y} height of containing + */ + sizeAfterTransform: function(width, height, options) { + var dimX = width / 2, dimY = height / 2, + points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + transformMatrix = fabric.util.calcDimensionsMatrix(options), + bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); + return { + x: bbox.width, + y: bbox.height, + }; + } + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + var _join = Array.prototype.join, + commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }, + repeatedCommands = { + m: 'l', + M: 'L' + }; + function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { + var costh2 = fabric.util.cos(th2), + sinth2 = fabric.util.sin(th2), + costh3 = fabric.util.cos(th3), + sinth3 = fabric.util.sin(th3), + toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1, + toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1, + cp1X = fromX + mT * ( -cosTh * rx * sinth2 - sinTh * ry * costh2), + cp1Y = fromY + mT * ( -sinTh * rx * sinth2 + cosTh * ry * costh2), + cp2X = toX + mT * ( cosTh * rx * sinth3 + sinTh * ry * costh3), + cp2Y = toY + mT * ( sinTh * rx * sinth3 - cosTh * ry * costh3); + + return ['C', + cp1X, cp1Y, + cp2X, cp2Y, + toX, toY + ]; + } + + /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp + * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here + * http://mozilla.org/MPL/2.0/ + */ + function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) { + var PI = Math.PI, th = rotateX * PI / 180, + sinTh = fabric.util.sin(th), + cosTh = fabric.util.cos(th), + fromX = 0, fromY = 0; + + rx = Math.abs(rx); + ry = Math.abs(ry); + + var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5, + py = -cosTh * toY * 0.5 + sinTh * toX * 0.5, + rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px, + pl = rx2 * ry2 - rx2 * py2 - ry2 * px2, + root = 0; + + if (pl < 0) { + var s = Math.sqrt(1 - pl / (rx2 * ry2)); + rx *= s; + ry *= s; + } + else { + root = (large === sweep ? -1.0 : 1.0) * + Math.sqrt( pl / (rx2 * py2 + ry2 * px2)); + } + + var cx = root * rx * py / ry, + cy = -root * ry * px / rx, + cx1 = cosTh * cx - sinTh * cy + toX * 0.5, + cy1 = sinTh * cx + cosTh * cy + toY * 0.5, + mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry), + dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry); + + if (sweep === 0 && dtheta > 0) { + dtheta -= 2 * PI; + } + else if (sweep === 1 && dtheta < 0) { + dtheta += 2 * PI; + } + + // Convert into cubic bezier segments <= 90deg + var segments = Math.ceil(Math.abs(dtheta / PI * 2)), + result = [], mDelta = dtheta / segments, + mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2), + th3 = mTheta + mDelta; + + for (var i = 0; i < segments; i++) { + result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY); + fromX = result[i][5]; + fromY = result[i][6]; + mTheta = th3; + th3 += mDelta; + } + return result; + } + + /* + * Private + */ + function calcVectorAngle(ux, uy, vx, vy) { + var ta = Math.atan2(uy, ux), + tb = Math.atan2(vy, vx); + if (tb >= ta) { + return tb - ta; + } + else { + return 2 * Math.PI - (ta - tb); + } + } + + /** + * Calculate bounding box of a beziercurve + * @param {Number} x0 starting point + * @param {Number} y0 + * @param {Number} x1 first control point + * @param {Number} y1 + * @param {Number} x2 secondo control point + * @param {Number} y2 + * @param {Number} x3 end of bezier + * @param {Number} y3 + */ + // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. + // TODO: can we normalize this with the starting points set at 0 and then translated the bbox? + function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { + var argsString; + if (fabric.cachesBoundsOfCurve) { + argsString = _join.call(arguments); + if (fabric.boundsOfCurveCache[argsString]) { + return fabric.boundsOfCurveCache[argsString]; + } + } + + var sqrt = Math.sqrt, + min = Math.min, max = Math.max, + abs = Math.abs, tvalues = [], + bounds = [[], []], + a, b, c, t, t1, t2, b2ac, sqrtb2ac; + + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; + + for (var i = 0; i < 2; ++i) { + if (i > 0) { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } + + if (abs(a) < 1e-12) { + if (abs(b) < 1e-12) { + continue; + } + t = -c / b; + if (0 < t && t < 1) { + tvalues.push(t); + } + continue; + } + b2ac = b * b - 4 * c * a; + if (b2ac < 0) { + continue; + } + sqrtb2ac = sqrt(b2ac); + t1 = (-b + sqrtb2ac) / (2 * a); + if (0 < t1 && t1 < 1) { + tvalues.push(t1); + } + t2 = (-b - sqrtb2ac) / (2 * a); + if (0 < t2 && t2 < 1) { + tvalues.push(t2); + } + } + + var x, y, j = tvalues.length, jlen = j, mt; + while (j--) { + t = tvalues[j]; + mt = 1 - t; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; + + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; + } + + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; + var result = [ + { + x: min.apply(null, bounds[0]), + y: min.apply(null, bounds[1]) + }, + { + x: max.apply(null, bounds[0]), + y: max.apply(null, bounds[1]) + } + ]; + if (fabric.cachesBoundsOfCurve) { + fabric.boundsOfCurveCache[argsString] = result; + } + return result; + } + + /** + * Converts arc to a bunch of bezier curves + * @param {Number} fx starting point x + * @param {Number} fy starting point y + * @param {Array} coords Arc command + */ + function fromArcToBeziers(fx, fy, coords) { + var rx = coords[1], + ry = coords[2], + rot = coords[3], + large = coords[4], + sweep = coords[5], + tx = coords[6], + ty = coords[7], + segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segsNorm.length; i < len; i++) { + segsNorm[i][1] += fx; + segsNorm[i][2] += fy; + segsNorm[i][3] += fx; + segsNorm[i][4] += fy; + segsNorm[i][5] += fx; + segsNorm[i][6] += fy; + } + return segsNorm; + }; + + /** + * This function take a parsed SVG path and make it simpler for fabricJS logic. + * simplification consist of: only UPPERCASE absolute commands ( relative converted to absolute ) + * S converted in C, T converted in Q, A converted in C. + * @param {Array} path the array of commands of a parsed svg path for fabric.Path + * @return {Array} the simplified array of commands of a parsed svg path for fabric.Path + */ + function makePathSimpler(path) { + // x and y represent the last point of the path. the previous command point. + // we add them to each relative command to make it an absolute comment. + // we also swap the v V h H with L, because are easier to transform. + var x = 0, y = 0, len = path.length, + // x1 and y1 represent the last point of the subpath. the subpath is started with + // m or M command. When a z or Z command is drawn, x and y need to be resetted to + // the last x1 and y1. + x1 = 0, y1 = 0, current, i, converted, + // previous will host the letter of the previous command, to handle S and T. + // controlX and controlY will host the previous reflected control point + destinationPath = [], previous, controlX, controlY; + for (i = 0; i < len; ++i) { + converted = false; + current = path[i].slice(0); + switch (current[0]) { // first letter + case 'l': // lineto, relative + current[0] = 'L'; + current[1] += x; + current[2] += y; + // falls through + case 'L': + x = current[1]; + y = current[2]; + break; + case 'h': // horizontal lineto, relative + current[1] += x; + // falls through + case 'H': + current[0] = 'L'; + current[2] = y; + x = current[1]; + break; + case 'v': // vertical lineto, relative + current[1] += y; + // falls through + case 'V': + current[0] = 'L'; + y = current[1]; + current[1] = x; + current[2] = y; + break; + case 'm': // moveTo, relative + current[0] = 'M'; + current[1] += x; + current[2] += y; + // falls through + case 'M': + x = current[1]; + y = current[2]; + x1 = current[1]; + y1 = current[2]; + break; + case 'c': // bezierCurveTo, relative + current[0] = 'C'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + current[5] += x; + current[6] += y; + // falls through + case 'C': + controlX = current[3]; + controlY = current[4]; + x = current[5]; + y = current[6]; + break; + case 's': // shorthand cubic bezierCurveTo, relative + current[0] = 'S'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'S': + // would be sScC but since we are swapping sSc for C, we check just that. + if (previous === 'C') { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + x = current[3]; + y = current[4]; + current[0] = 'C'; + current[5] = current[3]; + current[6] = current[4]; + current[3] = current[1]; + current[4] = current[2]; + current[1] = controlX; + current[2] = controlY; + // current[3] and current[4] are NOW the second control point. + // we keep it for the next reflection. + controlX = current[3]; + controlY = current[4]; + break; + case 'q': // quadraticCurveTo, relative + current[0] = 'Q'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'Q': + controlX = current[1]; + controlY = current[2]; + x = current[3]; + y = current[4]; + break; + case 't': // shorthand quadraticCurveTo, relative + current[0] = 'T'; + current[1] += x; + current[2] += y; + // falls through + case 'T': + if (previous === 'Q') { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + current[0] = 'Q'; + x = current[1]; + y = current[2]; + current[1] = controlX; + current[2] = controlY; + current[3] = x; + current[4] = y; + break; + case 'a': + current[0] = 'A'; + current[6] += x; + current[7] += y; + // falls through + case 'A': + converted = true; + destinationPath = destinationPath.concat(fromArcToBeziers(x, y, current)); + x = current[6]; + y = current[7]; + break; + case 'z': + case 'Z': + x = x1; + y = y1; + break; + default: + } + if (!converted) { + destinationPath.push(current); + } + previous = current[0]; + } + return destinationPath; + }; + + /** + * Calc length from point x1,y1 to x2,y2 + * @param {Number} x1 starting point x + * @param {Number} y1 starting point y + * @param {Number} x2 starting point x + * @param {Number} y2 starting point y + * @return {Number} length of segment + */ + function calcLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + + // functions for the Cubic beizer + // taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350 + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + + function getPointOnCubicBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function(pct) { + var c1 = CB1(pct), c2 = CB2(pct), c3 = CB3(pct), c4 = CB4(pct); + return { + x: p4x * c1 + p3x * c2 + p2x * c3 + p1x * c4, + y: p4y * c1 + p3y * c2 + p2y * c3 + p1y * c4 + }; + }; + } + + function getTangentCubicIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (3 * invT * invT * (p2x - p1x)) + (6 * invT * pct * (p3x - p2x)) + + (3 * pct * pct * (p4x - p3x)), + tangentY = (3 * invT * invT * (p2y - p1y)) + (6 * invT * pct * (p3y - p2y)) + + (3 * pct * pct * (p4y - p3y)); + return Math.atan2(tangentY, tangentX); + }; + } + + function QB1(t) { + return t * t; + } + + function QB2(t) { + return 2 * t * (1 - t); + } + + function QB3(t) { + return (1 - t) * (1 - t); + } + + function getPointOnQuadraticBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function(pct) { + var c1 = QB1(pct), c2 = QB2(pct), c3 = QB3(pct); + return { + x: p3x * c1 + p2x * c2 + p1x * c3, + y: p3y * c1 + p2y * c2 + p1y * c3 + }; + }; + } + + function getTangentQuadraticIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (2 * invT * (p2x - p1x)) + (2 * pct * (p3x - p2x)), + tangentY = (2 * invT * (p2y - p1y)) + (2 * pct * (p3y - p2y)); + return Math.atan2(tangentY, tangentX); + }; + } + + + // this will run over a path segment ( a cubic or quadratic segment) and approximate it + // with 100 segemnts. This will good enough to calculate the length of the curve + function pathIterator(iterator, x1, y1) { + var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc; + for (perc = 1; perc <= 100; perc += 1) { + p = iterator(perc / 100); + tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y); + tempP = p; + } + return tmpLen; + } + + /** + * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1 + * that correspond to that pixels run over the path. + * The percentage will be then used to find the correct point on the canvas for the path. + * @param {Array} segInfo fabricJS collection of information on a parsed path + * @param {Number} distance from starting point, in pixels. + * @return {Object} info object with x and y ( the point on canvas ) and angle, the tangent on that point; + */ + function findPercentageForDistance(segInfo, distance) { + var perc = 0, tmpLen = 0, iterator = segInfo.iterator, tempP = { x: segInfo.x, y: segInfo.y }, + p, nextLen, nextStep = 0.01, angleFinder = segInfo.angleFinder, lastPerc; + // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100 + // the path + while (tmpLen < distance && perc <= 1 && nextStep > 0.0001) { + p = iterator(perc); + lastPerc = perc; + nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y); + // compare tmpLen each cycle with distance, decide next perc to test. + if ((nextLen + tmpLen) > distance) { + // we discard this step and we make smaller steps. + nextStep /= 2; + perc -= nextStep; + } + else { + tempP = p; + perc += nextStep; + tmpLen += nextLen; + } + } + p.angle = angleFinder(lastPerc); + return p; + } + + /** + * Run over a parsed and simplifed path and extrac some informations. + * informations are length of each command and starting point + * @param {Array} path fabricJS parsed path commands + * @return {Array} path commands informations + */ + function getPathSegmentsInfo(path) { + var totalLength = 0, len = path.length, current, + //x2 and y2 are the coords of segment start + //x1 and y1 are the coords of the current point + x1 = 0, y1 = 0, x2 = 0, y2 = 0, info = [], iterator, tempInfo, angleFinder; + for (var i = 0; i < len; i++) { + current = path[i]; + tempInfo = { + x: x1, + y: y1, + command: current[0], + }; + switch (current[0]) { //first letter + case 'M': + tempInfo.length = 0; + x2 = x1 = current[1]; + y2 = y1 = current[2]; + break; + case 'L': + tempInfo.length = calcLineLength(x1, y1, current[1], current[2]); + x1 = current[1]; + y1 = current[2]; + break; + case 'C': + iterator = getPointOnCubicBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + angleFinder = getTangentCubicIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[5]; + y1 = current[6]; + break; + case 'Q': + iterator = getPointOnQuadraticBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + angleFinder = getTangentQuadraticIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[3]; + y1 = current[4]; + break; + case 'Z': + case 'z': + // we add those in order to ease calculations later + tempInfo.destX = x2; + tempInfo.destY = y2; + tempInfo.length = calcLineLength(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + break; + } + totalLength += tempInfo.length; + info.push(tempInfo); + } + info.push({ length: totalLength, x: x1, y: y1 }); + return info; + } + + function getPointOnPath(path, distance, infos) { + if (!infos) { + infos = getPathSegmentsInfo(path); + } + var i = 0; + while ((distance - infos[i].length > 0) && i < (infos.length - 2)) { + distance -= infos[i].length; + i++; + } + // var distance = infos[infos.length - 1] * perc; + var segInfo = infos[i], segPercent = distance / segInfo.length, + command = segInfo.command, segment = path[i], info; + + switch (command) { + case 'M': + return { x: segInfo.x, y: segInfo.y, angle: 0 }; + case 'Z': + case 'z': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segInfo.destX, segInfo.destY), + segPercent + ); + info.angle = Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x); + return info; + case 'L': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segment[1], segment[2]), + segPercent + ); + info.angle = Math.atan2(segment[2] - segInfo.y, segment[1] - segInfo.x); + return info; + case 'C': + return findPercentageForDistance(segInfo, distance); + case 'Q': + return findPercentageForDistance(segInfo, distance); + } + } + + /** + * + * @param {string} pathString + * @return {(string|number)[][]} An array of SVG path commands + * @example Usage + * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [ + * ['M', 3, 4], + * ['Q', 3, 5, 2, 1, 4, 0], + * ['Q', 9, 12, 2, 1, 4, 0], + * ]; + * + */ + function parsePath(pathString) { + var result = [], + coords = [], + currentPath, + parsed, + re = fabric.rePathCommand, + rNumber = '[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?\\s*', + rNumberCommaWsp = '(' + rNumber + ')' + fabric.commaWsp, + rFlagCommaWsp = '([01])' + fabric.commaWsp + '?', + rArcSeq = rNumberCommaWsp + '?' + rNumberCommaWsp + '?' + rNumberCommaWsp + rFlagCommaWsp + rFlagCommaWsp + + rNumberCommaWsp + '?(' + rNumber + ')', + regArcArgumentSequence = new RegExp(rArcSeq, 'g'), + match, + coordsStr, + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + path; + if (!pathString || !pathString.match) { + return result; + } + path = pathString.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + for (var i = 0, coordsParsed, len = path.length; i < len; i++) { + currentPath = path[i]; + + coordsStr = currentPath.slice(1).trim(); + coords.length = 0; + + var command = currentPath.charAt(0); + coordsParsed = [command]; + + if (command.toLowerCase() === 'a') { + // arcs have special flags that apparently don't require spaces so handle special + for (var args; (args = regArcArgumentSequence.exec(coordsStr));) { + for (var j = 1; j < args.length; j++) { + coords.push(args[j]); + } + } + } + else { + while ((match = re.exec(coordsStr))) { + coords.push(match[0]); + } + } + + for (var j = 0, jlen = coords.length; j < jlen; j++) { + parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + coordsParsed.push(parsed); + } + } + + var commandLength = commandLengths[command.toLowerCase()], + repeatedCommand = repeatedCommands[command] || command; + + if (coordsParsed.length - 1 > commandLength) { + for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { + result.push([command].concat(coordsParsed.slice(k, k + commandLength))); + command = repeatedCommand; + } + } + else { + result.push(coordsParsed); + } + } + + return result; + }; + + /** + * + * Converts points to a smooth SVG path + * @param {{ x: number,y: number }[]} points Array of points + * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value. + * @return {(string|number)[][]} An array of SVG path commands + */ + function getSmoothPathFromPoints(points, correction) { + var path = [], i, + p1 = new fabric.Point(points[0].x, points[0].y), + p2 = new fabric.Point(points[1].x, points[1].y), + len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; + correction = correction || 0; + + if (manyPoints) { + multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; + multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; + } + path.push(['M', p1.x - multSignX * correction, p1.y - multSignY * correction]); + for (i = 1; i < len; i++) { + if (!p1.eq(p2)) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]); + } + p1 = points[i]; + if ((i + 1) < points.length) { + p2 = points[i + 1]; + } + } + if (manyPoints) { + multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; + multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; + } + path.push(['L', p1.x + multSignX * correction, p1.y + multSignY * correction]); + return path; + } + /** + * Transform a path by transforming each segment. + * it has to be a simplified path or it won't work. + * WARNING: this depends from pathOffset for correct operation + * @param {Array} path fabricJS parsed and simplified path commands + * @param {Array} transform matrix that represent the transformation + * @param {Object} [pathOffset] the fabric.Path pathOffset + * @param {Number} pathOffset.x + * @param {Number} pathOffset.y + * @returns {Array} the transformed path + */ + function transformPath(path, transform, pathOffset) { + if (pathOffset) { + transform = fabric.util.multiplyTransformMatrices( + transform, + [1, 0, 0, 1, -pathOffset.x, -pathOffset.y] + ); + } + return path.map(function(pathSegment) { + var newSegment = pathSegment.slice(0), point = {}; + for (var i = 1; i < pathSegment.length - 1; i += 2) { + point.x = pathSegment[i]; + point.y = pathSegment[i + 1]; + point = fabric.util.transformPoint(point, transform); + newSegment[i] = point.x; + newSegment[i + 1] = point.y; + } + return newSegment; + }); + } + + /** + * Calculate bounding box of a elliptic-arc + * @deprecated + * @param {Number} fx start point of arc + * @param {Number} fy + * @param {Number} rx horizontal radius + * @param {Number} ry vertical radius + * @param {Number} rot angle of horizontal axis + * @param {Number} large 1 or 0, whatever the arc is the big or the small on the 2 points + * @param {Number} sweep 1 or 0, 1 clockwise or counterclockwise direction + * @param {Number} tx end point of arc + * @param {Number} ty + */ + function getBoundsOfArc(fx, fy, rx, ry, rot, large, sweep, tx, ty) { + + var fromX = 0, fromY = 0, bound, bounds = [], + segs = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segs.length; i < len; i++) { + bound = getBoundsOfCurve(fromX, fromY, segs[i][1], segs[i][2], segs[i][3], segs[i][4], segs[i][5], segs[i][6]); + bounds.push({ x: bound[0].x + fx, y: bound[0].y + fy }); + bounds.push({ x: bound[1].x + fx, y: bound[1].y + fy }); + fromX = segs[i][5]; + fromY = segs[i][6]; + } + return bounds; + }; + + /** + * Draws arc + * @deprecated + * @param {CanvasRenderingContext2D} ctx + * @param {Number} fx + * @param {Number} fy + * @param {Array} coords coords of the arc, without the front 'A/a' + */ + function drawArc(ctx, fx, fy, coords) { + coords = coords.slice(0).unshift('X'); // command A or a does not matter + var beziers = fromArcToBeziers(fx, fy, coords); + beziers.forEach(function(bezier) { + ctx.bezierCurveTo.apply(ctx, bezier.slice(1)); + }); + }; + + /** + * Join path commands to go back to svg format + * @param {Array} pathData fabricJS parsed path commands + * @return {String} joined path 'M 0 0 L 20 30' + */ + fabric.util.joinPath = function(pathData) { + return pathData.map(function (segment) { return segment.join(' '); }).join(' '); + }; + fabric.util.parsePath = parsePath; + fabric.util.makePathSimpler = makePathSimpler; + fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints; + fabric.util.getPathSegmentsInfo = getPathSegmentsInfo; + fabric.util.getBoundsOfCurve = getBoundsOfCurve; + fabric.util.getPointOnPath = getPointOnPath; + fabric.util.transformPath = transformPath; + /** + * Typo of `fromArcToBeziers` kept for not breaking the api once corrected. + * Will be removed in fabric 5.0 + * @deprecated + */ + fabric.util.fromArcToBeizers = fromArcToBeziers; + // kept because we do not want to make breaking changes. + // but useless and deprecated. + fabric.util.getBoundsOfArc = getBoundsOfArc; + fabric.util.drawArc = drawArc; +})(); + + +(function() { + + var slice = Array.prototype.slice; + + /** + * Invokes method on all items in a given array + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + * @return {Array} + */ + function invoke(array, method) { + var args = slice.call(arguments, 2), result = []; + for (var i = 0, len = array.length; i < len; i++) { + result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); + } + return result; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function max(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function fill(array, value) { + var k = array.length; + while (k--) { + array[k] = value; + } + return array; + } + + /** + * @private + */ + function find(array, byProperty, condition) { + if (!array || array.length === 0) { + return; + } + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + if (byProperty) { + while (i--) { + if (condition(array[i][byProperty], result)) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (condition(array[i], result)) { + result = array[i]; + } + } + } + return result; + } + + /** + * @namespace fabric.util.array + */ + fabric.util.array = { + fill: fill, + invoke: invoke, + min: min, + max: max + }; + +})(); + + +(function() { + /** + * Copies all enumerable properties of one js object to another + * this does not and cannot compete with generic utils. + * Does not clone or extend fabric.Object subclasses. + * This is mostly for internal use and has extra handling for fabricJS objects + * it skips the canvas and group properties in deep cloning. + * @memberOf fabric.util.object + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + * @param {Boolean} [deep] Whether to extend nested objects + * @return {Object} + */ + + function extend(destination, source, deep) { + // JScript DontEnum bug is not taken care of + // the deep clone is for internal use, is not meant to avoid + // javascript traps or cloning html element or self referenced objects. + if (deep) { + if (!fabric.isLikelyNode && source instanceof Element) { + // avoid cloning deep images, canvases, + destination = source; + } + else if (source instanceof Array) { + destination = []; + for (var i = 0, len = source.length; i < len; i++) { + destination[i] = extend({ }, source[i], deep); + } + } + else if (source && typeof source === 'object') { + for (var property in source) { + if (property === 'canvas' || property === 'group') { + // we do not want to clone this props at all. + // we want to keep the keys in the copy + destination[property] = null; + } + else if (source.hasOwnProperty(property)) { + destination[property] = extend({ }, source[property], deep); + } + } + } + else { + // this sounds odd for an extend but is ok for recursive use + destination = source; + } + } + else { + for (var property in source) { + destination[property] = source[property]; + } + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * @memberOf fabric.util.object + * @param {Object} object Object to clone + * @param {Boolean} [deep] Whether to clone nested objects + * @return {Object} + */ + + //TODO: this function return an empty object if you try to clone null + function clone(object, deep) { + return extend({ }, object, deep); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + fabric.util.object.extend(fabric.util, fabric.Observable); +})(); + + +(function() { + + /** + * Camelizes a string + * @memberOf fabric.util.string + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ + function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); + } + + /** + * Capitalizes a string + * @memberOf fabric.util.string + * @param {String} string String to capitalize + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. + * @return {String} Capitalized version of a string + */ + function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); + } + + /** + * Escapes XML in a string + * @memberOf fabric.util.string + * @param {String} string String to escape + * @return {String} Escaped version of a string + */ + function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + /** + * Divide a string in the user perceived single units + * @memberOf fabric.util.string + * @param {String} textstring String to escape + * @return {Array} array containing the graphemes + */ + function graphemeSplit(textstring) { + var i = 0, chr, graphemes = []; + for (i = 0, chr; i < textstring.length; i++) { + if ((chr = getWholeChar(textstring, i)) === false) { + continue; + } + graphemes.push(chr); + } + return graphemes; + } + + // taken from mdn in the charAt doc page. + function getWholeChar(str, i) { + var code = str.charCodeAt(i); + + if (isNaN(code)) { + return ''; // Position not found + } + if (code < 0xD800 || code > 0xDFFF) { + return str.charAt(i); + } + + // High surrogate (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 <= code && code <= 0xDBFF) { + if (str.length <= (i + 1)) { + throw 'High surrogate without following low surrogate'; + } + var next = str.charCodeAt(i + 1); + if (0xDC00 > next || next > 0xDFFF) { + throw 'High surrogate without following low surrogate'; + } + return str.charAt(i) + str.charAt(i + 1); + } + // Low surrogate (0xDC00 <= code && code <= 0xDFFF) + if (i === 0) { + throw 'Low surrogate without preceding high surrogate'; + } + var prev = str.charCodeAt(i - 1); + + // (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 > prev || prev > 0xDBFF) { + throw 'Low surrogate without preceding high surrogate'; + } + // We can pass over low surrogates now as the second component + // in a pair which we have already processed + return false; + } + + + /** + * String utilities + * @namespace fabric.util.string + */ + fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml, + graphemeSplit: graphemeSplit + }; +})(); + + +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }, + + IS_DONTENUM_BUGGY = (function() { + for (var p in { toString: 1 }) { + if (p === 'toString') { + return false; + } + } + return true; + })(), + + /** @ignore */ + addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + function callSuper(methodName) { + var parentMethod = null, + _this = this; + + // climb prototype chain to find method not equal to callee's method + while (_this.constructor.superclass) { + var superClassMethod = _this.constructor.superclass.prototype[methodName]; + if (_this[methodName] !== superClassMethod) { + parentMethod = superClassMethod; + break; + } + // eslint-disable-next-line + _this = _this.constructor.superclass.prototype; + } + + if (!parentMethod) { + return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); + } + + return (arguments.length > 1) + ? parentMethod.apply(this, slice.call(arguments, 1)) + : parentMethod.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param {Function} [parent] optional "Class" to inherit from + * @param {Object} [properties] Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; + } + + fabric.util.createClass = createClass; +})(); + + +(function () { + // since ie11 can use addEventListener but they do not support options, i need to check + var couldUseAttachEvent = !!fabric.document.createElement('div').attachEvent, + touchEvents = ['touchstart', 'touchmove', 'touchend']; + /** + * Adds an event listener to an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = function(element, eventName, handler, options) { + element && element.addEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + /** + * Removes an event listener from an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = function(element, eventName, handler, options) { + element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + function getTouchInfo(event) { + var touchProp = event.changedTouches; + if (touchProp && touchProp[0]) { + return touchProp[0]; + } + return event; + } + + fabric.util.getPointer = function(event) { + var element = event.target, + scroll = fabric.util.getScrollLeftTop(element), + _evt = getTouchInfo(event); + return { + x: _evt.clientX + scroll.left, + y: _evt.clientY + scroll.top + }; + }; + + fabric.util.isTouchEvent = function(event) { + return touchEvents.indexOf(event.type) > -1 || event.pointerType === 'touch'; + }; +})(); + + +(function () { + + /** + * Cross-browser wrapper for setting element's style + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {Object} styles + * @return {HTMLElement} Element that was passed as a first argument + */ + function setStyle(element, styles) { + var elementStyle = element.style; + if (!elementStyle) { + return element; + } + if (typeof styles === 'string') { + element.style.cssText += ';' + styles; + return styles.indexOf('opacity') > -1 + ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) + : element; + } + for (var property in styles) { + if (property === 'opacity') { + setOpacity(element, styles[property]); + } + else { + var normalizedProperty = (property === 'float' || property === 'cssFloat') + ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') + : property; + elementStyle[normalizedProperty] = styles[property]; + } + } + return element; + } + + var parseEl = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, + + /** @ignore */ + setOpacity = function (element) { return element; }; + + if (supportsOpacity) { + /** @ignore */ + setOpacity = function(element, value) { + element.style.opacity = value; + return element; + }; + } + else if (supportsFilters) { + /** @ignore */ + setOpacity = function(element, value) { + var es = element.style; + if (element.currentStyle && !element.currentStyle.hasLayout) { + es.zoom = 1; + } + if (reOpacity.test(es.filter)) { + value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')'); + es.filter = es.filter.replace(reOpacity, value); + } + else { + es.filter += ' alpha(opacity=' + (value * 100) + ')'; + } + return element; + }; + } + + fabric.util.setStyle = setStyle; + +})(); + + +(function() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + var sliceCanConvertNodelists, + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch (err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + + /** + * Adds class to an element + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if (element && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + function wrapElement(element, wrapper, attributes) { + if (typeof wrapper === 'string') { + wrapper = makeElement(wrapper, attributes); + } + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); + } + wrapper.appendChild(element); + return wrapper; + } + + /** + * Returns element scroll offsets + * @memberOf fabric.util + * @param {HTMLElement} element Element to operate on + * @return {Object} Object with left/top values + */ + function getScrollLeftTop(element) { + + var left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + // While loop checks (and then sets element to) .parentNode OR .host + // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, + // but the .parentNode of a root ShadowDOM node will always be null, instead + // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 + while (element && (element.parentNode || element.host)) { + + // Set element to element parent, or 'host' in case of ShadowDOM + element = element.parentNode || element.host; + + if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + + if (element.nodeType === 1 && element.style.position === 'fixed') { + break; + } + } + + return { left: left, top: top }; + } + + /** + * Returns offset for a given element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + var docElem, + doc = element && element.ownerDocument, + box = { left: 0, top: 0 }, + offset = { left: 0, top: 0 }, + scrollLeftTop, + offsetAttributes = { + borderLeftWidth: 'left', + borderTopWidth: 'top', + paddingLeft: 'left', + paddingTop: 'top' + }; + + if (!doc) { + return offset; + } + + for (var attr in offsetAttributes) { + offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; + } + + docElem = doc.documentElement; + if ( typeof element.getBoundingClientRect !== 'undefined' ) { + box = element.getBoundingClientRect(); + } + + scrollLeftTop = getScrollLeftTop(element); + + return { + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top + }; + } + + /** + * Returns style attribute value of a given element + * @memberOf fabric.util + * @param {HTMLElement} element Element to get style attribute for + * @param {String} attr Style attribute to get for element + * @return {String} Style attribute value of the given element. + */ + var getElementStyle; + if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { + getElementStyle = function(element, attr) { + var style = fabric.document.defaultView.getComputedStyle(element, null); + return style ? style[attr] : undefined; + }; + } + else { + getElementStyle = function(element, attr) { + var value = element.style[attr]; + if (!value && element.currentStyle) { + value = element.currentStyle[attr]; + } + return value; + }; + } + + (function () { + var style = fabric.document.documentElement.style, + selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = fabric.util.falseFunction; + } + if (selectProp) { + element.style[selectProp] = 'none'; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = 'on'; + } + return element; + } + + /** + * Makes element selectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + function getNodeCanvas(element) { + var impl = fabric.jsdomImplForWrapper(element); + return impl._canvas || impl._image; + }; + + function cleanUpJsdomNode(element) { + if (!fabric.isLikelyNode) { + return; + } + var impl = fabric.jsdomImplForWrapper(element); + if (impl) { + impl._image = null; + impl._canvas = null; + // unsure if necessary + impl._currentSrc = null; + impl._attributes = null; + impl._classList = null; + } + } + + function setImageSmoothing(ctx, value) { + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled + || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; + ctx.imageSmoothingEnabled = value; + } + + /** + * setImageSmoothing sets the context imageSmoothingEnabled property. + * Used by canvas and by ImageObject. + * @memberOf fabric.util + * @since 4.0.0 + * @param {HTMLRenderingContext2D} ctx to set on + * @param {Boolean} value true or false + */ + fabric.util.setImageSmoothing = setImageSmoothing; + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.addClass = addClass; + fabric.util.makeElement = makeElement; + fabric.util.wrapElement = wrapElement; + fabric.util.getScrollLeftTop = getScrollLeftTop; + fabric.util.getElementOffset = getElementOffset; + fabric.util.getNodeCanvas = getNodeCanvas; + fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; + +})(); + + +(function() { + + function addParamToUrl(url, param) { + return url + (/\?/.test(url) ? '&' : '?') + param; + } + + function emptyFn() { } + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {String} [options.parameters] parameters to append to url in GET or in body + * @param {String} [options.body] body to send with POST or PUT request + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = new fabric.window.XMLHttpRequest(), + body = options.body || options.parameters; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); + + +/** + * Wrapper around `console.log` (when available) + * @param {*} [values] Values to log + */ +fabric.log = console.log; + +/** + * Wrapper around `console.warn` (when available) + * @param {*} [values] Values to log as a warning + */ +fabric.warn = console.warn; + + +(function() { + + function noop() { + return false; + } + + function defaultEasing(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change (in ms) + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ + function animate(options) { + var cancel = false; + requestAnimFrame(function(timestamp) { + options || (options = { }); + + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || noop, + abort = options.abort || noop, + onComplete = options.onComplete || noop, + easing = options.easing || defaultEasing, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick(ticktime) { + // TODO: move abort call after calculation + // and pass (current,valuePerc, timePerc) as arguments + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start), + timePerc = currentTime / duration, + current = easing(currentTime, startValue, byValue, duration), + valuePerc = Math.abs((current - startValue) / byValue); + if (cancel) { + return; + } + if (abort(current, valuePerc, timePerc)) { + // remove this in 4.0 + // does to even make sense to abort and run onComplete? + onComplete(endValue, 1, 1); + return; + } + if (time > finish) { + onChange(endValue, 1, 1); + onComplete(endValue, 1, 1); + return; + } + else { + onChange(current, valuePerc, timePerc); + requestAnimFrame(tick); + } + })(start); + }); + return function() { + cancel = true; + }; + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback) { + return fabric.window.setTimeout(callback, 1000 / 60); + }; + + var _cancelAnimFrame = fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; + + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + function requestAnimFrame() { + return _requestAnimFrame.apply(fabric.window, arguments); + } + + function cancelAnimFrame() { + return _cancelAnimFrame.apply(fabric.window, arguments); + } + + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.cancelAnimFrame = cancelAnimFrame; +})(); + + +(function() { + // Calculate an in-between color. Returns a "rgba()" string. + // Credit: Edwin Martin + // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js + function calculateColor(begin, end, pos) { + var color = 'rgba(' + + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' + + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' + + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); + + color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); + color += ')'; + return color; + } + + /** + * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {String} fromColor The starting color in hex or rgb(a) format. + * @param {String} toColor The starting color in hex or rgb(a) format. + * @param {Number} [duration] Duration of change (in ms). + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + * @returns {Function} abort function + */ + function animateColor(fromColor, toColor, duration, options) { + var startColor = new fabric.Color(fromColor).getSource(), + endColor = new fabric.Color(toColor).getSource(), + originalOnComplete = options.onComplete, + originalOnChange = options.onChange; + options = options || {}; + + return fabric.util.animate(fabric.util.object.extend(options, { + duration: duration || 500, + startValue: startColor, + endValue: endColor, + byValue: endColor, + easing: function (currentTime, startValue, byValue, duration) { + var posValue = options.colorEasing + ? options.colorEasing(currentTime, duration) + : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); + return calculateColor(startValue, byValue, posValue); + }, + // has to take in account for color restoring; + onComplete: function(current, valuePerc, timePerc) { + if (originalOnComplete) { + return originalOnComplete( + calculateColor(endColor, endColor, 0), + valuePerc, + timePerc + ); + } + }, + onChange: function(current, valuePerc, timePerc) { + if (originalOnChange) { + if (Array.isArray(current)) { + return originalOnChange( + calculateColor(current, current, 0), + valuePerc, + timePerc + ); + } + originalOnChange(current, valuePerc, timePerc); + } + } + })); + } + + fabric.util.animateColor = animateColor; + +})(); + + +(function() { + + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + //handle the 0/0 case: + if (c === 0 && a === 0) { + s = p / (2 * Math.PI) * Math.asin(1); + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + } + return { a: a, c: c, p: p, s: s }; + } + + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); + } + + /** + * Cubic easing out + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c * ((t = t / d - 1) * t * t + 1) + b; + } + + /** + * Cubic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t + 2) + b; + } + + /** + * Quartic easing in + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c * (t /= d) * t * t * t + b; + } + + /** + * Quartic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t = t / d - 1) * t * t * t - 1) + b; + } + + /** + * Quartic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t + b; + } + return -c / 2 * ((t -= 2) * t * t * t - 2) + b; + } + + /** + * Quintic easing in + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + } + + /** + * Quintic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + } + + /** + * Quintic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + } + + /** + * Sinusoidal easing in + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Sinusoidal easing out + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t / d * (Math.PI / 2)) + b; + } + + /** + * Sinusoidal easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + } + + /** + * Exponential easing in + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + } + + /** + * Exponential easing out + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + } + + /** + * Exponential easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t === 0) { + return b; + } + if (t === d) { + return b + c; + } + t /= d / 2; + if (t < 1) { + return c / 2 * Math.pow(2, 10 * (t - 1)) + b; + } + return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * Circular easing in + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + } + + /** + * Circular easing out + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + } + + /** + * Circular easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; + } + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + } + + /** + * Elastic easing in + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; + } + + /** + * Elastic easing out + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; + } + + /** + * Elastic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d / 2; + if (t === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + var opts = normalize(a, c, p, s); + if (t < 1) { + return -0.5 * elastic(opts, t, d) + b; + } + return opts.a * Math.pow(2, -10 * (t -= 1)) * + Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.c + b; + } + + /** + * Backwards easing in + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * (t /= d) * t * ((s + 1) * t - s) + b; + } + + /** + * Backwards easing out + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + } + + /** + * Backwards easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + t /= d / 2; + if (t < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + } + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + } + + /** + * Bouncing easing in + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d - t, 0, c, d) + b; + } + + /** + * Bouncing easing out + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } + else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; + } + else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; + } + } + + /** + * Bouncing easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d / 2) { + return easeInBounce (t * 2, 0, c, d) * 0.5 + b; + } + return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + + /** + * Easing functions + * See Easing Equations by Robert Penner + * @namespace fabric.util.ease + */ + fabric.util.ease = { + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c * (t /= d) * t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d / 2); + if (t < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c * (t /= d) * t * t + b; + }, + + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +})(); + + +(function(global) { + + 'use strict'; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + parseUnit = fabric.util.parseUnit, + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + + svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line', + 'image', 'text'], + svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'], + svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'], + svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'], + + attributesMap = { + cx: 'left', + x: 'left', + r: 'radius', + cy: 'top', + y: 'top', + display: 'visible', + visibility: 'visible', + transform: 'transformMatrix', + 'fill-opacity': 'fillOpacity', + 'fill-rule': 'fillRule', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-style': 'fontStyle', + 'font-weight': 'fontWeight', + 'letter-spacing': 'charSpacing', + 'paint-order': 'paintFirst', + 'stroke-dasharray': 'strokeDashArray', + 'stroke-dashoffset': 'strokeDashOffset', + 'stroke-linecap': 'strokeLineCap', + 'stroke-linejoin': 'strokeLineJoin', + 'stroke-miterlimit': 'strokeMiterLimit', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'text-decoration': 'textDecoration', + 'text-anchor': 'textAnchor', + opacity: 'opacity', + 'clip-path': 'clipPath', + 'clip-rule': 'clipRule', + 'vector-effect': 'strokeUniform', + 'image-rendering': 'imageSmoothing', + }, + + colorAttributes = { + stroke: 'strokeOpacity', + fill: 'fillOpacity' + }, + + fSize = 'font-size', cPath = 'clip-path'; + + fabric.svgValidTagNamesRegEx = getSvgRegex(svgValidTagNames); + fabric.svgViewBoxElementsRegEx = getSvgRegex(svgViewBoxElements); + fabric.svgInvalidAncestorsRegEx = getSvgRegex(svgInvalidAncestors); + fabric.svgValidParentsRegEx = getSvgRegex(svgValidParents); + + fabric.cssRules = { }; + fabric.gradientDefs = { }; + fabric.clipPaths = { }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + function normalizeValue(attr, value, parentAttributes, fontSize) { + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; + + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + else if (attr === 'strokeUniform') { + return (value === 'non-scaling-stroke'); + } + else if (attr === 'strokeDashArray') { + if (value === 'none') { + value = null; + } + else { + value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); + } + } + else if (attr === 'transformMatrix') { + if (parentAttributes && parentAttributes.transformMatrix) { + value = multiplyTransformMatrices( + parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); + } + else { + value = fabric.parseTransformAttribute(value); + } + } + else if (attr === 'visible') { + value = value !== 'none' && value !== 'hidden'; + // display=none on parent element always takes precedence over child element + if (parentAttributes && parentAttributes.visible === false) { + value = false; + } + } + else if (attr === 'opacity') { + value = parseFloat(value); + if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { + value *= parentAttributes.opacity; + } + } + else if (attr === 'textAnchor' /* text-anchor */) { + value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; + } + else if (attr === 'charSpacing') { + // parseUnit returns px and we convert it to em + parsed = parseUnit(value, fontSize) / fontSize * 1000; + } + else if (attr === 'paintFirst') { + var fillIndex = value.indexOf('fill'); + var strokeIndex = value.indexOf('stroke'); + var value = 'fill'; + if (fillIndex > -1 && strokeIndex > -1 && strokeIndex < fillIndex) { + value = 'stroke'; + } + else if (fillIndex === -1 && strokeIndex > -1) { + value = 'stroke'; + } + } + else if (attr === 'href' || attr === 'xlink:href' || attr === 'font') { + return value; + } + else if (attr === 'imageSmoothing') { + return (value === 'optimizeQuality'); + } + else { + parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); + } + + return (!isArray && isNaN(parsed) ? value : parsed); + } + + /** + * @private + */ + function getSvgRegex(arr) { + return new RegExp('^(' + arr.join('|') + ')\\b', 'i'); + } + + /** + * @private + * @param {Object} attributes Array of attributes to parse + */ + function _setStrokeFillOpacity(attributes) { + for (var attr in colorAttributes) { + + if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { + continue; + } + + if (typeof attributes[attr] === 'undefined') { + if (!fabric.Object.prototype[attr]) { + continue; + } + attributes[attr] = fabric.Object.prototype[attr]; + } + + if (attributes[attr].indexOf('url(') === 0) { + continue; + } + + var color = new fabric.Color(attributes[attr]); + attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); + } + return attributes; + } + + /** + * @private + */ + function _getMultipleNodes(doc, nodeNames) { + var nodeName, nodeArray = [], nodeList, i, len; + for (i = 0, len = nodeNames.length; i < len; i++) { + nodeName = nodeNames[i]; + nodeList = doc.getElementsByTagName(nodeName); + nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); + } + return nodeArray; + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix + */ + fabric.parseTransformAttribute = (function() { + function rotateMatrix(matrix, args) { + var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), + x = 0, y = 0; + if (args.length === 3) { + x = args[1]; + y = args[2]; + } + + matrix[0] = cos; + matrix[1] = sin; + matrix[2] = -sin; + matrix[3] = cos; + matrix[4] = x - (cos * x - sin * y); + matrix[5] = y - (sin * x + cos * y); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewMatrix(matrix, args, pos) { + matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); + } + + function translateMatrix(matrix, args) { + matrix[4] = args[0]; + if (args.length === 2) { + matrix[5] = args[1]; + } + } + + // identity matrix + var iMatrix = fabric.iMatrix, + + // == begin transform regexp + number = fabric.reNum, + + commaWsp = fabric.commaWsp, + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + ')' + + commaWsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', + + transformList = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transformList), + // == end transform regexp + + reTransform = new RegExp(transform, 'g'); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(), + matrices = []; + + // return if no argument was given or + // an argument does not match transform attribute regexp + if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { + return matrix; + } + + attributeValue.replace(reTransform, function(match) { + + var m = new RegExp(transform).exec(match).filter(function (match) { + // match !== '' && match != null + return (!!match); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch (operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + args[0] = fabric.util.degreesToRadians(args[0]); + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewMatrix(matrix, args, 2); + break; + case 'skewY': + skewMatrix(matrix, args, 1); + break; + case 'matrix': + matrix = args; + break; + } + + // snapshot current matrix into matrices array + matrices.push(matrix.concat()); + // reset + matrix = iMatrix.concat(); + }); + + var combinedMatrix = matrices[0]; + while (matrices.length > 1) { + matrices.shift(); + combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); + } + return combinedMatrix; + }; + })(); + + /** + * @private + */ + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); + + attr = pair[0].trim().toLowerCase(); + value = pair[1].trim(); + + oStyle[attr] = value; + }); + } + + /** + * @private + */ + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') { + continue; + } + + attr = prop.toLowerCase(); + value = style[prop]; + + oStyle[attr] = value; + } + } + + /** + * @private + */ + function getGlobalStylesForElement(element, svgUid) { + var styles = { }; + for (var rule in fabric.cssRules[svgUid]) { + if (elementMatchesRule(element, rule.split(' '))) { + for (var property in fabric.cssRules[svgUid][rule]) { + styles[property] = fabric.cssRules[svgUid][rule][property]; + } + } + } + return styles; + } + + /** + * @private + */ + function elementMatchesRule(element, selectors) { + var firstMatching, parentMatching = true; + //start from rightmost selector. + firstMatching = selectorMatches(element, selectors.pop()); + if (firstMatching && selectors.length) { + parentMatching = doesSomeParentMatch(element, selectors); + } + return firstMatching && parentMatching && (selectors.length === 0); + } + + function doesSomeParentMatch(element, selectors) { + var selector, parentMatching = true; + while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { + if (parentMatching) { + selector = selectors.pop(); + } + element = element.parentNode; + parentMatching = selectorMatches(element, selector); + } + return selectors.length === 0; + } + + /** + * @private + */ + function selectorMatches(element, selector) { + var nodeName = element.nodeName, + classNames = element.getAttribute('class'), + id = element.getAttribute('id'), matcher, i; + // i check if a selector matches slicing away part from it. + // if i get empty string i should match + matcher = new RegExp('^' + nodeName, 'i'); + selector = selector.replace(matcher, ''); + if (id && selector.length) { + matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + if (classNames && selector.length) { + classNames = classNames.split(' '); + for (i = classNames.length; i--;) { + matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + } + return selector.length === 0; + } + + /** + * @private + * to support IE8 missing getElementById on SVGdocument and on node xmlDOM + */ + function elementById(doc, id) { + var el; + doc.getElementById && (el = doc.getElementById(id)); + if (el) { + return el; + } + var node, i, len, nodelist = doc.getElementsByTagName('*'); + for (i = 0, len = nodelist.length; i < len; i++) { + node = nodelist[i]; + if (id === node.getAttribute('id')) { + return node; + } + } + } + + /** + * @private + */ + function parseUseDirectives(doc) { + var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; + while (nodelist.length && i < nodelist.length) { + var el = nodelist[i], + xlinkAttribute = el.getAttribute('xlink:href') || el.getAttribute('href'); + + if (xlinkAttribute === null) { + return; + } + + var xlink = xlinkAttribute.substr(1), + x = el.getAttribute('x') || 0, + y = el.getAttribute('y') || 0, + el2 = elementById(doc, xlink).cloneNode(true), + currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', + parentNode, + oldLength = nodelist.length, attr, + j, + attrs, + len, + namespace = fabric.svgNS; + + applyViewboxTransform(el2); + if (/^svg$/i.test(el2.nodeName)) { + var el3 = el2.ownerDocument.createElementNS(namespace, 'g'); + for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + el3.setAttributeNS(namespace, attr.nodeName, attr.nodeValue); + } + // el2.firstChild != null + while (el2.firstChild) { + el3.appendChild(el2.firstChild); + } + el2 = el3; + } + + for (j = 0, attrs = el.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + if (attr.nodeName === 'x' || attr.nodeName === 'y' || + attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { + continue; + } + + if (attr.nodeName === 'transform') { + currentTrans = attr.nodeValue + ' ' + currentTrans; + } + else { + el2.setAttribute(attr.nodeName, attr.nodeValue); + } + } + + el2.setAttribute('transform', currentTrans); + el2.setAttribute('instantiated_by_use', '1'); + el2.removeAttribute('id'); + parentNode = el.parentNode; + parentNode.replaceChild(el2, el); + // some browsers do not shorten nodelist after replaceChild (IE8) + if (nodelist.length === oldLength) { + i++; + } + } + } + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // matches, e.g.: +14.56e-12, etc. + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*' + + '$' + ); + + /** + * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements + */ + function applyViewboxTransform(element) { + if (!fabric.svgViewBoxElementsRegEx.test(element.nodeName)) { + return {}; + } + var viewBoxAttr = element.getAttribute('viewBox'), + scaleX = 1, + scaleY = 1, + minX = 0, + minY = 0, + viewBoxWidth, viewBoxHeight, matrix, el, + widthAttr = element.getAttribute('width'), + heightAttr = element.getAttribute('height'), + x = element.getAttribute('x') || 0, + y = element.getAttribute('y') || 0, + preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', + missingViewBox = (!viewBoxAttr || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), + missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), + toBeParsed = missingViewBox && missingDimAttr, + parsedDim = { }, translateMatrix = '', widthDiff = 0, heightDiff = 0; + + parsedDim.width = 0; + parsedDim.height = 0; + parsedDim.toBeParsed = toBeParsed; + + if (missingViewBox) { + if (((x || y) && element.parentNode && element.parentNode.nodeName !== '#document')) { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + matrix = (element.getAttribute('transform') || '') + translateMatrix; + element.setAttribute('transform', matrix); + element.removeAttribute('x'); + element.removeAttribute('y'); + } + } + + if (toBeParsed) { + return parsedDim; + } + + if (missingViewBox) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + // set a transform for elements that have x y and are inner(only) SVGs + return parsedDim; + } + minX = -parseFloat(viewBoxAttr[1]); + minY = -parseFloat(viewBoxAttr[2]); + viewBoxWidth = parseFloat(viewBoxAttr[3]); + viewBoxHeight = parseFloat(viewBoxAttr[4]); + parsedDim.minX = minX; + parsedDim.minY = minY; + parsedDim.viewBoxWidth = viewBoxWidth; + parsedDim.viewBoxHeight = viewBoxHeight; + if (!missingDimAttr) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + scaleX = parsedDim.width / viewBoxWidth; + scaleY = parsedDim.height / viewBoxHeight; + } + else { + parsedDim.width = viewBoxWidth; + parsedDim.height = viewBoxHeight; + } + + // default is to preserve aspect ratio + preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); + if (preserveAspectRatio.alignX !== 'none') { + //translate all container for the effect of Mid, Min, Max + if (preserveAspectRatio.meetOrSlice === 'meet') { + scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); + // calculate additional translation to move the viewbox + } + if (preserveAspectRatio.meetOrSlice === 'slice') { + scaleY = scaleX = (scaleX > scaleY ? scaleX : scaleY); + // calculate additional translation to move the viewbox + } + widthDiff = parsedDim.width - viewBoxWidth * scaleX; + heightDiff = parsedDim.height - viewBoxHeight * scaleX; + if (preserveAspectRatio.alignX === 'Mid') { + widthDiff /= 2; + } + if (preserveAspectRatio.alignY === 'Mid') { + heightDiff /= 2; + } + if (preserveAspectRatio.alignX === 'Min') { + widthDiff = 0; + } + if (preserveAspectRatio.alignY === 'Min') { + heightDiff = 0; + } + } + + if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { + return parsedDim; + } + if ((x || y) && element.parentNode.nodeName !== '#document') { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + } + + matrix = translateMatrix + ' matrix(' + scaleX + + ' 0' + + ' 0 ' + + scaleY + ' ' + + (minX * scaleX + widthDiff) + ' ' + + (minY * scaleY + heightDiff) + ') '; + // seems unused. + // parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); + if (element.nodeName === 'svg') { + el = element.ownerDocument.createElementNS(fabric.svgNS, 'g'); + // element.firstChild != null + while (element.firstChild) { + el.appendChild(element.firstChild); + } + element.appendChild(el); + } + else { + el = element; + el.removeAttribute('x'); + el.removeAttribute('y'); + matrix = el.getAttribute('transform') + matrix; + } + el.setAttribute('transform', matrix); + return parsedDim; + } + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) + && !element.getAttribute('instantiated_by_use')) { + return true; + } + } + return false; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; + * It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [parsingOptions] options for parsing document + * @param {String} [parsingOptions.crossOrigin] crossOrigin settings + */ + fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { + if (!doc) { + return; + } + + parseUseDirectives(doc); + + var svgUid = fabric.Object.__uid++, i, len, + options = applyViewboxTransform(doc), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; + options.svgUid = svgUid; + + if (descendants.length === 0 && fabric.isLikelyNode) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes('//*[name(.)!="svg"]'); + var arr = []; + for (i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + applyViewboxTransform(el); + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')) && + !hasAncestorWithNodeName(el, fabric.svgInvalidAncestorsRegEx); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + if (!elements || (elements && !elements.length)) { + callback && callback([], {}); + return; + } + var clipPaths = { }; + descendants.filter(function(el) { + return el.nodeName.replace('svg:', '') === 'clipPath'; + }).forEach(function(el) { + var id = el.getAttribute('id'); + clipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) { + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); + }); + }); + fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + fabric.clipPaths[svgUid] = clipPaths; + // Precedence of rules: style > class > attribute + fabric.parseElements(elements, function(instances, elements) { + if (callback) { + callback(instances, options, elements, descendants); + delete fabric.gradientDefs[svgUid]; + delete fabric.cssRules[svgUid]; + delete fabric.clipPaths[svgUid]; + } + }, clone(options), reviver, parsingOptions); + }; + + function recursivelyParseGradientsXlink(doc, gradient) { + var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], + xlinkAttr = 'xlink:href', + xLink = gradient.getAttribute(xlinkAttr).substr(1), + referencedGradient = elementById(doc, xLink); + if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { + recursivelyParseGradientsXlink(doc, referencedGradient); + } + gradientsAttrs.forEach(function(attr) { + if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { + gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); + } + }); + if (!gradient.children.length) { + var referenceClone = referencedGradient.cloneNode(true); + while (referenceClone.firstChild) { + gradient.appendChild(referenceClone.firstChild); + } + } + gradient.removeAttribute(xlinkAttr); + } + + var reFontDeclaration = new RegExp( + '(normal|italic)?\\s*(normal|small-caps)?\\s*' + + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + + fabric.reNum + + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); + + extend(fabric, { + /** + * Parses a short font declaration, building adding its properties to a style object + * @static + * @function + * @memberOf fabric + * @param {String} value font declaration + * @param {Object} oStyle definition + */ + parseFontDeclaration: function(value, oStyle) { + var match = value.match(reFontDeclaration); + + if (!match) { + return; + } + var fontStyle = match[1], + // font variant is not used + // fontVariant = match[2], + fontWeight = match[3], + fontSize = match[4], + lineHeight = match[5], + fontFamily = match[6]; + + if (fontStyle) { + oStyle.fontStyle = fontStyle; + } + if (fontWeight) { + oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); + } + if (fontSize) { + oStyle.fontSize = parseUnit(fontSize); + } + if (fontFamily) { + oStyle.fontFamily = fontFamily; + } + if (lineHeight) { + oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var tagArray = [ + 'linearGradient', + 'radialGradient', + 'svg:linearGradient', + 'svg:radialGradient'], + elList = _getMultipleNodes(doc, tagArray), + el, j = 0, gradientDefs = { }; + j = elList.length; + while (j--) { + el = elList[j]; + if (el.getAttribute('xlink:href')) { + recursivelyParseGradientsXlink(doc, el); + } + gradientDefs[el.getAttribute('id')] = el; + } + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes, svgUid) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }, + fontSize, parentFontSize; + + if (typeof svgUid === 'undefined') { + svgUid = element.getAttribute('svgUid'); + } + // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards + if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { // eslint-disable-line + memo[attr] = value; + } + return memo; + }, { }); + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + var cssAttrs = extend( + getGlobalStylesForElement(element, svgUid), + fabric.parseStyleAttribute(element) + ); + ownAttributes = extend( + ownAttributes, + cssAttrs + ); + if (cssAttrs[cPath]) { + element.setAttribute(cPath, cssAttrs[cPath]); + } + fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; + if (ownAttributes[fSize]) { + // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. + ownAttributes[fSize] = fontSize = parseUnit(ownAttributes[fSize], parentFontSize); + } + + var normalizedAttr, normalizedValue, normalizedStyle = {}; + for (var attr in ownAttributes) { + normalizedAttr = normalizeAttr(attr); + normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); + normalizedStyle[normalizedAttr] = normalizedValue; + } + if (normalizedStyle && normalizedStyle.font) { + fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); + } + var mergedAttrs = extend(parentAttributes, normalizedStyle); + return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver, parsingOptions) { + new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) { + return oStyle; + } + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param {String} points points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) { + return null; + } + + // replace commas with whitespace and remove bookending whitespace + points = points.replace(/,/g, ' ').trim(); + + points = points.split(/\s+/); + var parsedPoints = [], i, len; + + for (i = 0, len = points.length; i < len; i += 2) { + parsedPoints.push({ + x: parseFloat(points[i]), + y: parseFloat(points[i + 1]) + }); + } + + // odd number of points is an error + // if (parsedPoints.length % 2 !== 0) { + // return null; + // } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), i, len, + allRules = { }, rules; + + // very crude parsing of style contents + for (i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[i].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + if (styleContents.trim() === '') { + continue; + } + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + // eslint-disable-next-line no-loop-func + rules.forEach(function(rule) { + + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), + ruleObj = { }, declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + for (i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + ruleObj[property] = value; + } + rule = match[1]; + rule.split(',').forEach(function(_rule) { + _rule = _rule.replace(/^svg/i, '').trim(); + if (_rule === '') { + return; + } + if (allRules[_rule]) { + fabric.util.object.extend(allRules[_rule], ruleObj); + } + else { + allRules[_rule] = fabric.util.object.clone(ruleObj); + } + }); + }); + } + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. + * Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberOf fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromURL: function(url, callback, reviver, options) { + + url = url.replace(/^\n\s*/, '').trim(); + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml || !xml.documentElement) { + callback && callback(null); + return false; + } + + fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { + callback && callback(results, _options, elements, allElements); + }, reviver, options); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberOf fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromString: function(string, callback, reviver, options) { + var parser = new fabric.window.DOMParser(), + doc = parser.parseFromString(string.trim(), 'text/xml'); + fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { + callback(results, _options, elements, allElements); + }, reviver, options); + } + }); + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions, doc) { + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + this.svgUid = (options && options.svgUid) || 0; + this.parsingOptions = parsingOptions; + this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g; + this.doc = doc; +}; + +(function(proto) { + proto.parse = function() { + this.instances = new Array(this.elements.length); + this.numElements = this.elements.length; + this.createObjects(); + }; + + proto.createObjects = function() { + var _this = this; + this.elements.forEach(function(element, i) { + element.setAttribute('svgUid', _this.svgUid); + _this.createObject(element, i); + }); + }; + + proto.findTag = function(el) { + return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; + }; + + proto.createObject = function(el, index) { + var klass = this.findTag(el); + if (klass && klass.fromElement) { + try { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + catch (err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }; + + proto.createCallback = function(index, el) { + var _this = this; + return function(obj) { + var _options; + _this.resolveGradient(obj, el, 'fill'); + _this.resolveGradient(obj, el, 'stroke'); + if (obj instanceof fabric.Image && obj._originalElement) { + _options = obj.parsePreserveAspectRatioAttribute(el); + } + obj._removeTransformMatrix(_options); + _this.resolveClipPath(obj, el); + _this.reviver && _this.reviver(el, obj); + _this.instances[index] = obj; + _this.checkIfDone(); + }; + }; + + proto.extractPropertyDefinition = function(obj, property, storage) { + var value = obj[property], regex = this.regexUrl; + if (!regex.test(value)) { + return; + } + regex.lastIndex = 0; + var id = regex.exec(value)[1]; + regex.lastIndex = 0; + return fabric[storage][this.svgUid][id]; + }; + + proto.resolveGradient = function(obj, el, property) { + var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); + if (gradientDef) { + var opacityAttr = el.getAttribute(property + '-opacity'); + var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); + obj.set(property, gradient); + } + }; + + proto.createClipPathCallback = function(obj, container) { + return function(_newObj) { + _newObj._removeTransformMatrix(); + _newObj.fillRule = _newObj.clipRule; + container.push(_newObj); + }; + }; + + proto.resolveClipPath = function(obj, usingElement) { + var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'), + element, klass, objTransformInv, container, gTransform, options; + if (clipPath) { + container = []; + objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix()); + // move the clipPath tag as sibling to the real element that is using it + var clipPathTag = clipPath[0].parentNode; + var clipPathOwner = usingElement; + while (clipPathOwner.parentNode && clipPathOwner.getAttribute('clip-path') !== obj.clipPath) { + clipPathOwner = clipPathOwner.parentNode; + } + clipPathOwner.parentNode.appendChild(clipPathTag); + for (var i = 0; i < clipPath.length; i++) { + element = clipPath[i]; + klass = this.findTag(element); + klass.fromElement( + element, + this.createClipPathCallback(obj, container), + this.options + ); + } + if (container.length === 1) { + clipPath = container[0]; + } + else { + clipPath = new fabric.Group(container); + } + gTransform = fabric.util.multiplyTransformMatrices( + objTransformInv, + clipPath.calcTransformMatrix() + ); + if (clipPath.clipPath) { + this.resolveClipPath(clipPath, clipPathOwner); + } + var options = fabric.util.qrDecompose(gTransform); + clipPath.flipX = false; + clipPath.flipY = false; + clipPath.set('scaleX', options.scaleX); + clipPath.set('scaleY', options.scaleY); + clipPath.angle = options.angle; + clipPath.skewX = options.skewX; + clipPath.skewY = 0; + clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center'); + obj.clipPath = clipPath; + } + else { + // if clip-path does not resolve to any element, delete the property. + delete obj.clipPath; + } + }; + + proto.checkIfDone = function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + // eslint-disable-next-line no-eq-null, eqeqeq + return el != null; + }); + this.callback(this.instances, this.elements); + } + }; +})(fabric.ElementsParser.prototype); + + +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Point) { + fabric.warn('fabric.Point is already defined'); + return; + } + + fabric.Point = Point; + + /** + * Point class + * @class fabric.Point + * @memberOf fabric + * @constructor + * @param {Number} x + * @param {Number} y + * @return {fabric.Point} thisArg + */ + function Point(x, y) { + this.x = x; + this.y = y; + } + + Point.prototype = /** @lends fabric.Point.prototype */ { + + type: 'point', + + constructor: Point, + + /** + * Adds another point to this one and returns another one + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * Adds another point to this one + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * Adds value to this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * Adds value to this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * Subtracts another point from this point and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * Subtracts another point from this point + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + subtractEquals: function (that) { + this.x -= that.x; + this.y -= that.y; + return this; + }, + + /** + * Subtracts value from this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + scalarSubtract: function (scalar) { + return new Point(this.x - scalar, this.y - scalar); + }, + + /** + * Subtracts value from this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarSubtractEquals: function (scalar) { + this.x -= scalar; + this.y -= scalar; + return this; + }, + + /** + * Multiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + multiply: function (scalar) { + return new Point(this.x * scalar, this.y * scalar); + }, + + /** + * Multiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + multiplyEquals: function (scalar) { + this.x *= scalar; + this.y *= scalar; + return this; + }, + + /** + * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + divide: function (scalar) { + return new Point(this.x / scalar, this.y / scalar); + }, + + /** + * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + divideEquals: function (scalar) { + this.x /= scalar; + this.y /= scalar; + return this; + }, + + /** + * Returns true if this point is equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + eq: function (that) { + return (this.x === that.x && this.y === that.y); + }, + + /** + * Returns true if this point is less than another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lt: function (that) { + return (this.x < that.x && this.y < that.y); + }, + + /** + * Returns true if this point is less than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lte: function (that) { + return (this.x <= that.x && this.y <= that.y); + }, + + /** + + * Returns true if this point is greater another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gt: function (that) { + return (this.x > that.x && this.y > that.y); + }, + + /** + * Returns true if this point is greater than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gte: function (that) { + return (this.x >= that.x && this.y >= that.y); + }, + + /** + * Returns new point which is the result of linear interpolation with this one and another one + * @param {fabric.Point} that + * @param {Number} t , position of interpolation, between 0 and 1 default 0.5 + * @return {fabric.Point} + */ + lerp: function (that, t) { + if (typeof t === 'undefined') { + t = 0.5; + } + t = Math.max(Math.min(1, t), 0); + return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); + }, + + /** + * Returns distance from this point and another one + * @param {fabric.Point} that + * @return {Number} + */ + distanceFrom: function (that) { + var dx = this.x - that.x, + dy = this.y - that.y; + return Math.sqrt(dx * dx + dy * dy); + }, + + /** + * Returns the point between this point and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + midPointFrom: function (that) { + return this.lerp(that); + }, + + /** + * Returns a new point which is the min of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + min: function (that) { + return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); + }, + + /** + * Returns a new point which is the max of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + max: function (that) { + return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); + }, + + /** + * Returns string representation of this point + * @return {String} + */ + toString: function () { + return this.x + ',' + this.y; + }, + + /** + * Sets x/y of this point + * @param {Number} x + * @param {Number} y + * @chainable + */ + setXY: function (x, y) { + this.x = x; + this.y = y; + return this; + }, + + /** + * Sets x of this point + * @param {Number} x + * @chainable + */ + setX: function (x) { + this.x = x; + return this; + }, + + /** + * Sets y of this point + * @param {Number} y + * @chainable + */ + setY: function (y) { + this.y = y; + return this; + }, + + /** + * Sets x/y of this point from another point + * @param {fabric.Point} that + * @chainable + */ + setFromPoint: function (that) { + this.x = that.x; + this.y = that.y; + return this; + }, + + /** + * Swaps x/y of this point and another point + * @param {fabric.Point} that + */ + swap: function (that) { + var x = this.x, + y = this.y; + this.x = that.x; + this.y = that.y; + that.x = x; + that.y = y; + }, + + /** + * return a cloned instance of the point + * @return {fabric.Point} + */ + clone: function () { + return new Point(this.x, this.y); + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + constructor: Intersection, + + /** + * Appends a point to intersection + * @param {fabric.Point} point + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoint: function (point) { + this.points.push(point); + return this; + }, + + /** + * Appends points to intersection + * @param {Array} points + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + return this; + } + }; + + /** + * Checks if one line intersects another + * TODO: rename in intersectSegmentSegment + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (uB !== 0) { + var ua = uaT / uB, + ub = ubT / uB; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection('Intersection'); + result.appendPoint(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (uaT === 0 || ubT === 0) { + result = new Intersection('Coincident'); + } + else { + result = new Intersection('Parallel'); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * TODO: rename in intersectSegmentPolygon + * fix detection of coincident + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1, a2, points) { + var result = new Intersection(), + length = points.length, + b1, b2, inter, i; + + for (i = 0; i < length; i++) { + b1 = points[i]; + b2 = points[(i + 1) % length]; + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length, i; + + for (i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i + 1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {fabric.Point} r1 + * @param {fabric.Point} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * Color class + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class fabric.Color + * @param {String} color optional in hex or rgb(a) or hsl format or from known color list + * @return {fabric.Color} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} + */ + function Color(color) { + if (!color) { + this.setSource([0, 0, 0, 1]); + } + else { + this._tryParsingColor(color); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @lends fabric.Color.prototype */ { + + /** + * @private + * @param {String|Array} color Color value to parse + */ + _tryParsingColor: function(color) { + var source; + + if (color in Color.colorNameMap) { + color = Color.colorNameMap[color]; + } + + if (color === 'transparent') { + source = [255, 255, 255, 0]; + } + + if (!source) { + source = Color.sourceFromHex(color); + } + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (!source) { + //if color is not recognize let's make black as canvas does + source = [0, 0, 0, 1]; + } + if (source) { + this.setSource(source); + } + }, + + /** + * Adapted from https://github.com/mjijackson + * @private + * @param {Number} r Red color value + * @param {Number} g Green color value + * @param {Number} b Blue color value + * @return {Array} Hsl color + */ + _rgbToHsl: function(r, g, b) { + r /= 255; g /= 255; b /= 255; + + var h, s, l, + max = fabric.util.array.max([r, g, b]), + min = fabric.util.array.min([r, g, b]); + + l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [ + Math.round(h * 360), + Math.round(s * 100), + Math.round(l * 100) + ]; + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color representation in RGB format + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color representation in RGBA format + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color representation in HSL format + * @return {String} ex: hsl(0-360,0%-100%,0%-100%) + */ + toHsl: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; + }, + + /** + * Returns color representation in HSLA format + * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) + */ + toHsla: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; + }, + + /** + * Returns color representation in HEX format + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(), r, g, b; + + r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Returns color representation in HEXA format + * @return {String} ex: FF5555CC + */ + toHexa: function() { + var source = this.getSource(), a; + + a = Math.round(source[3] * 255); + a = a.toString(16); + a = (a.length === 1) ? ('0' + a) : a; + + return this.toHex() + a.toUpperCase(); + }, + + /** + * Gets value of alpha channel for this color + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @param {Number} alpha Alpha value 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @return {fabric.Color} thisArg + */ + toGrayscale: function() { + var source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Transforms color to its black and white representation + * @param {Number} threshold + * @return {fabric.Color} thisArg + */ + toBlackWhite: function(threshold) { + var source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3]; + + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Overlays color with another color + * @param {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); + } + + var result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(), i; + + for (i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); + } + + result[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) + * @static + * @field + * @memberOf fabric.Color + */ + // eslint-disable-next-line max-len + fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HEX format (ex: #FF5544CC, #FF5555, 010155, aff) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; + + /** + * Map of the 148 color names with HEX code + * @static + * @field + * @memberOf fabric.Color + * @see: https://www.w3.org/TR/css3-color/#svg-color + */ + fabric.Color.colorNameMap = { + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aqua: '#00FFFF', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + black: '#000000', + blanchedalmond: '#FFEBCD', + blue: '#0000FF', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + cyan: '#00FFFF', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgrey: '#A9A9A9', + darkgreen: '#006400', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + fuchsia: '#FF00FF', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + gray: '#808080', + grey: '#808080', + green: '#008000', + greenyellow: '#ADFF2F', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgray: '#D3D3D3', + lightgrey: '#D3D3D3', + lightgreen: '#90EE90', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + lime: '#00FF00', + limegreen: '#32CD32', + linen: '#FAF0E6', + magenta: '#FF00FF', + maroon: '#800000', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + navy: '#000080', + oldlace: '#FDF5E6', + olive: '#808000', + olivedrab: '#6B8E23', + orange: '#FFA500', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + purple: '#800080', + rebeccapurple: '#663399', + red: '#FF0000', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + silver: '#C0C0C0', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + teal: '#008080', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + white: '#FFFFFF', + whitesmoke: '#F5F5F5', + yellow: '#FFFF00', + yellowgreen: '#9ACD32' + }; + + /** + * @private + * @param {Number} p + * @param {Number} q + * @param {Number} t + * @return {Number} + */ + function hue2rgb(p, q, t) { + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } + return p; + } + + /** + * Returns new color object, when given a color in RGB format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) + * @return {Array} source + */ + fabric.Color.sourceFromRgb = function(color) { + var match = color.match(Color.reRGBa); + if (match) { + var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), + g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), + b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); + + return [ + parseInt(r, 10), + parseInt(g, 10), + parseInt(b, 10), + match[4] ? parseFloat(match[4]) : 1 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HSL format + * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) + * @memberOf fabric.Color + * @return {fabric.Color} + */ + fabric.Color.fromHsl = function(color) { + return Color.fromSource(Color.sourceFromHsl(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. + * Adapted from https://github.com/mjijackson + * @memberOf fabric.Color + * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) + * @return {Array} source + * @see http://http://www.w3.org/TR/css3-color/#hsl-color + */ + fabric.Color.sourceFromHsl = function(color) { + var match = color.match(Color.reHSLa); + if (!match) { + return; + } + + var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, + s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), + l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), + r, g, b; + + if (s === 0) { + r = g = b = l; + } + else { + var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, + p = l * 2 - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255), + match[4] ? parseFloat(match[4]) : 1 + ]; + }; + + /** + * Returns new color object, when given a color in HSLA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromHsla = Color.fromHsl; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color Color value ex: FF5555 + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color ex: FF5555 or FF5544CC (RGBa) + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3 || value.length === 4), + isRGBa = (value.length === 8 || value.length === 4), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), + a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + parseFloat((parseInt(a, 16) / 255).toFixed(2)) + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @memberOf fabric.Color + * @param {Array} source + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + scaleMap = ['e', 'se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'], + skewMap = ['ns', 'nesw', 'ew', 'nwse'], + controls = {}, + LEFT = 'left', TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', CENTER = 'center', + opposite = { + top: BOTTOM, + bottom: TOP, + left: RIGHT, + right: LEFT, + center: CENTER, + }, radiansToDegrees = fabric.util.radiansToDegrees, + sign = (Math.sign || function(x) { return ((x > 0) - (x < 0)) || +x; }); + + /** + * Combine control position and object angle to find the control direction compared + * to the object center. + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + * @param {fabric.Control} control the control class + * @return {Number} 0 - 7 a quadrant number + */ + function findCornerQuadrant(fabricObject, control) { + var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + return Math.round((cornerAngle % 360) / 45); + } + + function fireEvent(eventName, options) { + var target = options.transform.target, + canvas = target.canvas, + canvasOptions = fabric.util.object.clone(options); + canvasOptions.target = target; + canvas && canvas.fire('object:' + eventName, canvasOptions); + target.fire(eventName, options); + } + + /** + * Inspect event and fabricObject properties to understand if the scaling action + * @param {Event} eventData from the user action + * @param {fabric.Object} fabricObject the fabric object about to scale + * @return {Boolean} true if scale is proportional + */ + function scaleIsProportional(eventData, fabricObject) { + var canvas = fabricObject.canvas, uniScaleKey = canvas.uniScaleKey, + uniformIsToggled = eventData[uniScaleKey]; + return (canvas.uniformScaling && !uniformIsToggled) || + (!canvas.uniformScaling && uniformIsToggled); + } + + /** + * Checks if transform is centered + * @param {Object} transform transform data + * @return {Boolean} true if transform is centered + */ + function isTransformCentered(transform) { + return transform.originX === CENTER && transform.originY === CENTER; + } + + /** + * Inspect fabricObject to understand if the current scaling action is allowed + * @param {fabric.Object} fabricObject the fabric object about to scale + * @param {String} by 'x' or 'y' or '' + * @param {Boolean} scaleProportionally true if we are trying to scale proportionally + * @return {Boolean} true if scaling is not allowed at current conditions + */ + function scalingIsForbidden(fabricObject, by, scaleProportionally) { + var lockX = fabricObject.lockScalingX, lockY = fabricObject.lockScalingY; + if (lockX && lockY) { + return true; + } + if (!by && (lockX || lockY) && scaleProportionally) { + return true; + } + if (lockX && by === 'x') { + return true; + } + if (lockY && by === 'y') { + return true; + } + return false; + } + + /** + * return the correct cursor style for the scale action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed', + scaleProportionally = scaleIsProportional(eventData, fabricObject), + by = ''; + if (control.x !== 0 && control.y === 0) { + by = 'x'; + } + else if (control.x === 0 && control.y !== 0) { + by = 'y'; + } + if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control); + return scaleMap[n] + '-resize'; + } + + /** + * return the correct cursor style for the skew action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function skewCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed'; + if (control.x !== 0 && fabricObject.lockSkewingY) { + return notAllowed; + } + if (control.y !== 0 && fabricObject.lockSkewingX) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control) % 4; + return skewMap[n] + '-resize'; + } + + /** + * Combine skew and scale style handlers to cover fabric standard use case + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleSkewCursorStyleHandler(eventData, control, fabricObject) { + if (eventData[fabricObject.canvas.altActionKey]) { + return controls.skewCursorStyleHandler(eventData, control, fabricObject); + } + return controls.scaleCursorStyleHandler(eventData, control, fabricObject); + } + + /** + * Inspect event, control and fabricObject to return the correct action name + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} an action name + */ + function scaleOrSkewActionName(eventData, control, fabricObject) { + var isAlternative = eventData[fabricObject.canvas.altActionKey]; + if (control.x === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewX' : 'scaleY'; + } + if (control.y === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewY' : 'scaleX'; + } + } + + /** + * Find the correct style for the control that is used for rotation. + * this function is very simple and it just take care of not-allowed or standard cursor + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function rotationStyleHandler(eventData, control, fabricObject) { + if (fabricObject.lockRotation) { + return 'not-allowed'; + } + return control.cursorStyle; + } + + function commonEventInfo(eventData, transform, x, y) { + return { + e: eventData, + transform: transform, + pointer: { + x: x, + y: y, + } + }; + } + + /** + * Wrap an action handler with saving/restoring object position on the transform. + * this is the code that permits to objects to keep their position while transforming. + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFixedAnchor(actionHandler) { + return function(eventData, transform, x, y) { + var target = transform.target, centerPoint = target.getCenterPoint(), + constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), + actionPerformed = actionHandler(eventData, transform, x, y); + target.setPositionByOrigin(constraint, transform.originX, transform.originY); + return actionPerformed; + }; + } + + /** + * Wrap an action handler with firing an event if the action is performed + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFireEvent(eventName, actionHandler) { + return function(eventData, transform, x, y) { + var actionPerformed = actionHandler(eventData, transform, x, y); + if (actionPerformed) { + fireEvent(eventName, commonEventInfo(eventData, transform, x, y)); + } + return actionPerformed; + }; + } + + /** + * Transforms a point described by x and y in a distance from the top left corner of the object + * bounding box. + * @param {Object} transform + * @param {String} originX + * @param {String} originY + * @param {number} x + * @param {number} y + * @return {Fabric.Point} the normalized point + */ + function getLocalPoint(transform, originX, originY, x, y) { + var target = transform.target, + control = target.controls[transform.corner], + zoom = target.canvas.getZoom(), + padding = target.padding / zoom, + localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); + if (localPoint.x >= padding) { + localPoint.x -= padding; + } + if (localPoint.x <= -padding) { + localPoint.x += padding; + } + if (localPoint.y >= padding) { + localPoint.y -= padding; + } + if (localPoint.y <= padding) { + localPoint.y += padding; + } + localPoint.x -= control.offsetX; + localPoint.y -= control.offsetY; + return localPoint; + } + + /** + * Detect if the fabric object is flipped on one side. + * @param {fabric.Object} target + * @return {Boolean} true if one flip, but not two. + */ + function targetHasOneFlip(target) { + return target.flipX !== target.flipY; + } + + /** + * Utility function to compensate the scale factor when skew is applied on both axes + * @private + */ + function compensateScaleForSkew(target, oppositeSkew, scaleToCompensate, axis, reference) { + if (target[oppositeSkew] !== 0) { + var newDim = target._getTransformedDimensions()[axis]; + var newValue = reference / newDim * target[scaleToCompensate]; + target.set(scaleToCompensate, newValue); + } + } + + /** + * Action handler for skewing on the X axis + * @private + */ + function skewObjectX(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(0, target.skewY), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.x * 2) - dimNoSkew.x, + currentSkew = target.skewX, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleX), (dimNoSkew.y / target.scaleY)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().y; + target.set('skewX', newSkew); + compensateScaleForSkew(target, 'skewY', 'scaleY', 'y', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Action handler for skewing on the Y axis + * @private + */ + function skewObjectY(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(target.skewX, 0), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.y * 2) - dimNoSkew.y, + currentSkew = target.skewY, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleY), (dimNoSkew.x / target.scaleX)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().x; + target.set('skewY', newSkew); + compensateScaleForSkew(target, 'skewX', 'scaleX', 'x', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerX(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewX > 0 and originY bottom we anchor on right + // if skewX > 0 and originY top we anchor on left + // if skewX < 0 and originY bottom we anchor on left + // if skewX < 0 and originY top we anchor on right + // if skewX is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewX, originX, originY = transform.originY; + if (target.lockSkewingX) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.x > 0) { + // we are pulling right, anchor left; + originX = LEFT; + } + else { + // we are pulling right, anchor right + originX = RIGHT; + } + } + else { + if (currentSkew > 0) { + originX = originY === TOP ? LEFT : RIGHT; + } + if (currentSkew < 0) { + originX = originY === TOP ? RIGHT : LEFT; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originX = originX === LEFT ? RIGHT : LEFT; + } + } + + // once we have the origin, we find the anchor point + transform.originX = originX; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectX)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerY(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewY > 0 and originX left we anchor on top + // if skewY > 0 and originX right we anchor on bottom + // if skewY < 0 and originX left we anchor on bottom + // if skewY < 0 and originX right we anchor on top + // if skewY is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewY, originY, originX = transform.originX; + if (target.lockSkewingY) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.y > 0) { + // we are pulling down, anchor up; + originY = TOP; + } + else { + // we are pulling up, anchor down + originY = BOTTOM; + } + } + else { + if (currentSkew > 0) { + originY = originX === LEFT ? TOP : BOTTOM; + } + if (currentSkew < 0) { + originY = originX === LEFT ? BOTTOM : TOP; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originY = originY === TOP ? BOTTOM : TOP; + } + } + + // once we have the origin, we find the anchor point + transform.originY = originY; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectY)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Action handler for rotation and snapping, without anchor point. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + * @private + */ + function rotationWithSnapping(eventData, transform, x, y) { + var t = transform, + target = t.target, + pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + + if (target.lockRotation) { + return false; + } + + var lastAngle = Math.atan2(t.ey - pivotPoint.y, t.ex - pivotPoint.x), + curAngle = Math.atan2(y - pivotPoint.y, x - pivotPoint.x), + angle = radiansToDegrees(curAngle - lastAngle + t.theta), + hasRotated = true; + + if (target.snapAngle > 0) { + var snapAngle = target.snapAngle, + snapThreshold = target.snapThreshold || snapAngle, + rightAngleLocked = Math.ceil(angle / snapAngle) * snapAngle, + leftAngleLocked = Math.floor(angle / snapAngle) * snapAngle; + + if (Math.abs(angle - leftAngleLocked) < snapThreshold) { + angle = leftAngleLocked; + } + else if (Math.abs(angle - rightAngleLocked) < snapThreshold) { + angle = rightAngleLocked; + } + } + + // normalize angle to positive value + if (angle < 0) { + angle = 360 + angle; + } + angle %= 360; + + hasRotated = target.angle !== angle; + target.angle = angle; + return hasRotated; + } + + /** + * Basic scaling logic, reused with different constrain for scaling X,Y, freely or equally. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @param {Object} options additional information for scaling + * @param {String} options.by 'x', 'y', 'equally' or '' to indicate type of scaling + * @return {Boolean} true if some change happened + * @private + */ + function scaleObject(eventData, transform, x, y, options) { + options = options || {}; + var target = transform.target, + lockScalingX = target.lockScalingX, lockScalingY = target.lockScalingY, + by = options.by, newPoint, scaleX, scaleY, dim, + scaleProportionally = scaleIsProportional(eventData, target), + forbidScaling = scalingIsForbidden(target, by, scaleProportionally), + signX, signY, gestureScale = transform.gestureScale; + + if (forbidScaling) { + return false; + } + if (gestureScale) { + scaleX = transform.scaleX * gestureScale; + scaleY = transform.scaleY * gestureScale; + } + else { + newPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y); + // use of sign: We use sign to detect change of direction of an action. sign usually change when + // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling + // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily + // cross many time the origin point and flip the object. so we need a way to filter out the noise. + // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. + signX = by !== 'y' ? sign(newPoint.x) : 1; + signY = by !== 'x' ? sign(newPoint.y) : 1; + if (!transform.signX) { + transform.signX = signX; + } + if (!transform.signY) { + transform.signY = signY; + } + + if (target.lockScalingFlip && + (transform.signX !== signX || transform.signY !== signY) + ) { + return false; + } + + dim = target._getTransformedDimensions(); + // missing detection of flip and logic to switch the origin + if (scaleProportionally && !by) { + // uniform scaling + var distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), + original = transform.original, + originalDistance = Math.abs(dim.x * original.scaleX / target.scaleX) + + Math.abs(dim.y * original.scaleY / target.scaleY), + scale = distance / originalDistance; + scaleX = original.scaleX * scale; + scaleY = original.scaleY * scale; + } + else { + scaleX = Math.abs(newPoint.x * target.scaleX / dim.x); + scaleY = Math.abs(newPoint.y * target.scaleY / dim.y); + } + // if we are scaling by center, we need to double the scale + if (isTransformCentered(transform)) { + scaleX *= 2; + scaleY *= 2; + } + if (transform.signX !== signX && by !== 'y') { + transform.originX = opposite[transform.originX]; + scaleX *= -1; + transform.signX = signX; + } + if (transform.signY !== signY && by !== 'x') { + transform.originY = opposite[transform.originY]; + scaleY *= -1; + transform.signY = signY; + } + } + // minScale is taken are in the setter. + var oldScaleX = target.scaleX, oldScaleY = target.scaleY; + if (!by) { + !lockScalingX && target.set('scaleX', scaleX); + !lockScalingY && target.set('scaleY', scaleY); + } + else { + // forbidden cases already handled on top here. + by === 'x' && target.set('scaleX', scaleX); + by === 'y' && target.set('scaleY', scaleY); + } + return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; + } + + /** + * Generic scaling logic, to scale from corners either equally or freely. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectFromCorner(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y); + } + + /** + * Scaling logic for the X axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectX(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'x' }); + } + + /** + * Scaling logic for the Y axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectY(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'y' }); + } + + /** + * Composed action handler to either scale Y or skew X + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingYOrSkewingX(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerX(eventData, transform, x, y); + } + return controls.scalingY(eventData, transform, x, y); + } + + /** + * Composed action handler to either scale X or skew Y + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingXOrSkewingY(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerY(eventData, transform, x, y); + } + return controls.scalingX(eventData, transform, x, y); + } + + /** + * Action handler to change textbox width + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function changeWidth(eventData, transform, x, y) { + var target = transform.target, localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), + multiplier = isTransformCentered(transform) ? 2 : 1, + oldWidth = target.width, + newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; + target.set('width', Math.max(newWidth, 0)); + return oldWidth !== newWidth; + } + + /** + * Action handler + * @private + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if the translation occurred + */ + function dragHandler(eventData, transform, x, y) { + var target = transform.target, + newLeft = x - transform.offsetX, + newTop = y - transform.offsetY, + moveX = !target.get('lockMovementX') && target.left !== newLeft, + moveY = !target.get('lockMovementY') && target.top !== newTop; + moveX && target.set('left', newLeft); + moveY && target.set('top', newTop); + if (moveX || moveY) { + fireEvent('moving', commonEventInfo(eventData, transform, x, y)); + } + return moveX || moveY; + } + + controls.scaleCursorStyleHandler = scaleCursorStyleHandler; + controls.skewCursorStyleHandler = skewCursorStyleHandler; + controls.scaleSkewCursorStyleHandler = scaleSkewCursorStyleHandler; + controls.rotationWithSnapping = wrapWithFireEvent('rotating', wrapWithFixedAnchor(rotationWithSnapping)); + controls.scalingEqually = wrapWithFireEvent('scaling', wrapWithFixedAnchor( scaleObjectFromCorner)); + controls.scalingX = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectX)); + controls.scalingY = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectY)); + controls.scalingYOrSkewingX = scalingYOrSkewingX; + controls.scalingXOrSkewingY = scalingXOrSkewingY; + controls.changeWidth = wrapWithFireEvent('resizing', wrapWithFixedAnchor(changeWidth)); + controls.skewHandlerX = skewHandlerX; + controls.skewHandlerY = skewHandlerY; + controls.dragHandler = dragHandler; + controls.scaleOrSkewActionName = scaleOrSkewActionName; + controls.rotationStyleHandler = rotationStyleHandler; + controls.fireEvent = fireEvent; + controls.wrapWithFixedAnchor = wrapWithFixedAnchor; + controls.wrapWithFireEvent = wrapWithFireEvent; + controls.getLocalPoint = getLocalPoint; + fabric.controlsUtils = controls; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + degreesToRadians = fabric.util.degreesToRadians, + controls = fabric.controlsUtils; + + /** + * Render a round control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderCircleControl (ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor), + myLeft = left, + myTop = top, size; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // as soon as fabric react v5, remove ie11, use proper ellipse code. + if (xSize > ySize) { + size = xSize; + ctx.scale(1.0, ySize / xSize); + myTop = top * xSize / ySize; + } + else if (ySize > xSize) { + size = ySize; + ctx.scale(xSize / ySize, 1.0); + myLeft = left * ySize / xSize; + } + else { + size = xSize; + } + // this is still wrong + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false); + ctx[methodName](); + if (stroke) { + ctx.stroke(); + } + ctx.restore(); + } + + /** + * Render a square control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderSquareControl(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && ( + styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor + ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // this is still wrong + ctx.lineWidth = 1; + ctx.translate(left, top); + ctx.rotate(degreesToRadians(fabricObject.angle)); + // this does not work, and fixed with ( && ) does not make sense. + // to have real transparent corners we need the controls on upperCanvas + // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize); + if (stroke) { + ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + } + ctx.restore(); + } + + controls.renderCircleControl = renderCircleControl; + controls.renderSquareControl = renderSquareControl; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + function Control(options) { + for (var i in options) { + this[i] = options[i]; + } + } + + fabric.Control = Control; + + fabric.Control.prototype = /** @lends fabric.Control.prototype */ { + + /** + * keep track of control visibility. + * mainly for backward compatibility. + * if you do not want to see a control, you can remove it + * from the controlset. + * @type {Boolean} + * @default true + */ + visible: true, + + /** + * Name of the action that the control will likely execute. + * This is optional. FabricJS uses to identify what the user is doing for some + * extra optimizations. If you are writing a custom control and you want to know + * somewhere else in the code what is going on, you can use this string here. + * you can also provide a custom getActionName if your control run multiple actions + * depending on some external state. + * default to scale since is the most common, used on 4 corners by default + * @type {String} + * @default 'scale' + */ + actionName: 'scale', + + /** + * Drawing angle of the control. + * NOT used for now, but name marked as needed for internal logic + * example: to reuse the same drawing function for different rotated controls + * @type {Number} + * @default 0 + */ + angle: 0, + + /** + * Relative position of the control. X + * 0,0 is the center of the Object, while -0.5 (left) or 0.5 (right) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + x: 0, + + /** + * Relative position of the control. Y + * 0,0 is the center of the Object, while -0.5 (top) or 0.5 (bottom) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + y: 0, + + /** + * Horizontal offset of the control from the defined position. In pixels + * Positive offset moves the control to the right, negative to the left. + * It used when you want to have position of control that does not scale with + * the bounding box. Example: rotation control is placed at x:0, y: 0.5 on + * the boundindbox, with an offset of 30 pixels vertically. Those 30 pixels will + * stay 30 pixels no matter how the object is big. Another example is having 2 + * controls in the corner, that stay in the same position when the object scale. + * of the bounding box. + * @type {Number} + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset of the control from the defined position. In pixels + * Positive offset moves the control to the bottom, negative to the top. + * @type {Number} + * @default 0 + */ + offsetY: 0, + + /** + * Sets the length of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeX: null, + + /** + * Sets the height of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeY: null, + + /** + * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeX: null, + + /** + * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeY: null, + + /** + * Css cursor style to display when the control is hovered. + * if the method `cursorStyleHandler` is provided, this property is ignored. + * @type {String} + * @default 'crosshair' + */ + cursorStyle: 'crosshair', + + /** + * If controls has an offsetY or offsetX, draw a line that connects + * the control to the bounding box + * @type {Boolean} + * @default false + */ + withConnection: false, + + /** + * The control actionHandler, provide one to handle action ( control being moved ) + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + actionHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control handler for mouse down, provide one to handle mouse down on control + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseDownHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control mouseUpHandler, provide one to handle an effect on mouse up. + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseUpHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * Returns control actionHandler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getActionHandler: function(/* eventData, fabricObject, control */) { + return this.actionHandler; + }, + + /** + * Returns control mouseDown handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseDownHandler: function(/* eventData, fabricObject, control */) { + return this.mouseDownHandler; + }, + + /** + * Returns control mouseUp handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseUpHandler: function(/* eventData, fabricObject, control */) { + return this.mouseUpHandler; + }, + + /** + * Returns control cursorStyle for css using cursorStyle. If you need a more elaborate + * function you can pass one in the constructor + * the cursorStyle property + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + cursorStyleHandler: function(eventData, control /* fabricObject */) { + return control.cursorStyle; + }, + + /** + * Returns the action name. The basic implementation just return the actionName property. + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + getActionName: function(eventData, control /* fabricObject */) { + return control.actionName; + }, + + /** + * Returns controls visibility + * @param {fabric.Object} object on which the control is displayed + * @param {String} controlKey key where the control is memorized on the + * @return {Boolean} + */ + getVisibility: function(fabricObject, controlKey) { + var objectVisibility = fabricObject._controlsVisibility; + if (objectVisibility && typeof objectVisibility[controlKey] !== 'undefined') { + return objectVisibility[controlKey]; + } + return this.visible; + }, + + /** + * Sets controls visibility + * @param {Boolean} visibility for the object + * @return {Void} + */ + setVisibility: function(visibility /* name, fabricObject */) { + this.visible = visibility; + }, + + + positionHandler: function(dim, finalMatrix /*, fabricObject, currentControl */) { + var point = fabric.util.transformPoint({ + x: this.x * dim.x + this.offsetX, + y: this.y * dim.y + this.offsetY }, finalMatrix); + return point; + }, + + /** + * Returns the coords for this control based on object values. + * @param {Number} objectAngle angle from the fabric object holding the control + * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if + * isTouch is true) + * @param {Number} centerX x coordinate where the control center should be + * @param {Number} centerY y coordinate where the control center should be + * @param {boolean} isTouch true if touch corner, false if normal corner + */ + calcCornerCoords: function(objectAngle, objectCornerSize, centerX, centerY, isTouch) { + var cosHalfOffset, + sinHalfOffset, + cosHalfOffsetComp, + sinHalfOffsetComp, + xSize = (isTouch) ? this.touchSizeX : this.sizeX, + ySize = (isTouch) ? this.touchSizeY : this.sizeY; + if (xSize && ySize && xSize !== ySize) { + // handle rectangular corners + var controlTriangleAngle = Math.atan2(ySize, xSize); + var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; + var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta); + // use complementary angle for two corners + cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp); + sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp); + } + else { + // handle square corners + // use default object corner size unless size is defined + var cornerSize = (xSize && ySize) ? xSize : objectCornerSize; + /* 0.7071067812 stands for sqrt(2)/2 */ + cornerHypotenuse = cornerSize * 0.7071067812; + // complementary angles are equal since they're both 45 degrees + var newTheta = fabric.util.degreesToRadians(45 - objectAngle); + cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta); + } + + return { + tl: { + x: centerX - sinHalfOffsetComp, + y: centerY - cosHalfOffsetComp, + }, + tr: { + x: centerX + cosHalfOffset, + y: centerY - sinHalfOffset, + }, + bl: { + x: centerX - cosHalfOffset, + y: centerY + sinHalfOffset, + }, + br: { + x: centerX + sinHalfOffsetComp, + y: centerY + cosHalfOffsetComp, + }, + }; + }, + + /** + * Render function for the control. + * When this function runs the context is unscaled. unrotate. Just retina scaled. + * all the functions will have to translate to the point left,top before starting Drawing + * if they want to draw a control where the position is detected. + * left and top are the result of the positionHandler function + * @param {RenderingContext2D} ctx the context where the control will be drawn + * @param {Number} left position of the canvas where we are about to render the control. + * @param {Number} top position of the canvas where we are about to render the control. + * @param {Object} styleOverride + * @param {fabric.Object} fabricObject the object where the control is about to be rendered + */ + render: function(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + switch (styleOverride.cornerStyle || fabricObject.cornerStyle) { + case 'circle': + fabric.controlsUtils.renderCircleControl.call(this, ctx, left, top, styleOverride, fabricObject); + break; + default: + fabric.controlsUtils.renderSquareControl.call(this, ctx, left, top, styleOverride, fabricObject); + } + }, + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + /* _FROM_SVG_START_ */ + function getColorStop(el, multiplier) { + var style = el.getAttribute('style'), + offset = el.getAttribute('offset') || 0, + color, colorAlpha, opacity, i; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length - 1] === '') { + keyValuePairs.pop(); + } + + for (i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + color = value; + } + else if (key === 'stop-opacity') { + opacity = value; + } + } + } + + if (!color) { + color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; + } + if (!opacity) { + opacity = el.getAttribute('stop-opacity'); + } + + color = new fabric.Color(color); + colorAlpha = color.getAlpha(); + opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); + opacity *= colorAlpha * multiplier; + + return { + offset: offset, + color: color.toRgb(), + opacity: opacity + }; + } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } + /* _FROM_SVG_END_ */ + + var clone = fabric.util.object.clone; + + /** + * Gradient class + * @class fabric.Gradient + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition + */ + fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { + + /** + * Horizontal offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetY: 0, + + /** + * A transform matrix to apply to the gradient before painting. + * Imported from svg gradients, is not applied with the current transform in the center. + * Before this transform is applied, the origin point is at the top left corner of the object + * plus the addition of offsetY and offsetX. + * @type Number[] + * @default null + */ + gradientTransform: null, + + /** + * coordinates units for coords. + * If `pixels`, the number of coords are in the same unit of width / height. + * If set as `percentage` the coords are still a number, but 1 means 100% of width + * for the X and 100% of the height for the y. It can be bigger than 1 and negative. + * allowed values pixels or percentage. + * @type String + * @default 'pixels' + */ + gradientUnits: 'pixels', + + /** + * Gradient type linear or radial + * @type String + * @default 'pixels' + */ + type: 'linear', + + /** + * Constructor + * @param {Object} options Options object with type, coords, gradientUnits and colorStops + * @param {Object} [options.type] gradient type linear or radial + * @param {Object} [options.gradientUnits] gradient units + * @param {Object} [options.offsetX] SVG import compatibility + * @param {Object} [options.offsetY] SVG import compatibility + * @param {Object[]} options.colorStops contains the colorstops. + * @param {Object} options.coords contains the coords of the gradient + * @param {Number} [options.coords.x1] X coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.y1] Y coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.x2] X coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.y2] Y coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.r1] only for radial gradient, radius of the inner circle + * @param {Number} [options.coords.r2] only for radial gradient, radius of the external circle + * @return {fabric.Gradient} thisArg + */ + initialize: function(options) { + options || (options = { }); + options.coords || (options.coords = { }); + + var coords, _this = this; + + // sets everything, then coords and colorstops get sets again + Object.keys(options).forEach(function(option) { + _this[option] = options[option]; + }); + + if (this.id) { + this.id += '_' + fabric.Object.__uid++; + } + else { + this.id = fabric.Object.__uid++; + } + + coords = { + x1: options.coords.x1 || 0, + y1: options.coords.y1 || 0, + x2: options.coords.x2 || 0, + y2: options.coords.y2 || 0 + }; + + if (this.type === 'radial') { + coords.r1 = options.coords.r1 || 0; + coords.r2 = options.coords.r2 || 0; + } + + this.coords = coords; + this.colorStops = options.colorStops.slice(); + }, + + /** + * Adds another colorStop + * @param {Object} colorStop Object with offset and color + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStops) { + for (var position in colorStops) { + var color = new fabric.Color(colorStops[position]); + this.colorStops.push({ + offset: parseFloat(position), + color: color.toRgb(), + opacity: color.getAlpha() + }); + } + return this; + }, + + /** + * Returns object representation of a gradient + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} + */ + toObject: function(propertiesToInclude) { + var object = { + type: this.type, + coords: this.coords, + colorStops: this.colorStops, + offsetX: this.offsetX, + offsetY: this.offsetY, + gradientUnits: this.gradientUnits, + gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an gradient + * @param {Object} object Object to create a gradient for + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object, options) { + var coords = clone(this.coords, true), i, len, options = options || {}, + markup, commonAttributes, colorStops = clone(this.colorStops, true), + needsSwap = coords.r1 > coords.r2, + transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), + offsetX = -this.offsetX, offsetY = -this.offsetY, + withViewport = !!options.additionalTransform, + gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; + // colorStops must be sorted ascending + colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (gradientUnits === 'objectBoundingBox') { + offsetX /= object.width; + offsetY /= object.height; + } + else { + offsetX += object.width / 2; + offsetY += object.height / 2; + } + if (object.type === 'path' && this.gradientUnits !== 'percentage') { + offsetX -= object.pathOffset.x; + offsetY -= object.pathOffset.y; + } + + + transform[4] -= offsetX; + transform[5] -= offsetY; + + commonAttributes = 'id="SVGID_' + this.id + + '" gradientUnits="' + gradientUnits + '"'; + commonAttributes += ' gradientTransform="' + (withViewport ? + options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; + + if (this.type === 'linear') { + markup = [ + '\n' + ]; + } + else if (this.type === 'radial') { + // svg radial gradient has just 1 radius. the biggest. + markup = [ + '\n' + ]; + } + + if (this.type === 'radial') { + if (needsSwap) { + // svg goes from internal to external radius. if radius are inverted, swap color stops. + colorStops = colorStops.concat(); + colorStops.reverse(); + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset = 1 - colorStops[i].offset; + } + } + var minRadius = Math.min(coords.r1, coords.r2); + if (minRadius > 0) { + // i have to shift all colorStops and add new one in 0. + var maxRadius = Math.max(coords.r1, coords.r2), + percentageShift = minRadius / maxRadius; + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); + } + } + } + + for (i = 0, len = colorStops.length; i < len; i++) { + var colorStop = colorStops[i]; + markup.push( + '\n' + ); + } + + markup.push((this.type === 'linear' ? '\n' : '\n')); + + return markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {CanvasGradient} + */ + toLive: function(ctx) { + var gradient, coords = fabric.util.object.clone(this.coords), i, len; + + if (!this.type) { + return; + } + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + coords.x1, coords.y1, coords.x2, coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); + } + + for (i = 0, len = this.colorStops.length; i < len; i++) { + var color = this.colorStops[i].color, + opacity = this.colorStops[i].opacity, + offset = this.colorStops[i].offset; + + if (typeof opacity !== 'undefined') { + color = new fabric.Color(color).setAlpha(opacity).toRgba(); + } + gradient.addColorStop(offset, color); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /* _FROM_SVG_START_ */ + /** + * Returns {@link fabric.Gradient} instance from an SVG element + * @static + * @memberOf fabric.Gradient + * @param {SVGGradientElement} el SVG gradient element + * @param {fabric.Object} instance + * @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. + * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly gradients + * that uses gradientUnits as 'userSpaceOnUse' and percentages. + * @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg + * @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg + * @param {Object.number} width width part of the svg tag if viewBox is not specified + * @param {Object.number} height height part of the svg tag if viewBox is not specified + * @return {fabric.Gradient} Gradient instance + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement + */ + fromElement: function(el, instance, opacityAttr, svgOptions) { + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + */ + + var multiplier = parseFloat(opacityAttr) / (/%$/.test(opacityAttr) ? 100 : 1); + multiplier = multiplier < 0 ? 0 : multiplier > 1 ? 1 : multiplier; + if (isNaN(multiplier)) { + multiplier = 1; + } + + var colorStopEls = el.getElementsByTagName('stop'), + type, + gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : 'percentage', + gradientTransform = el.getAttribute('gradientTransform') || '', + colorStops = [], + coords, i, offsetX = 0, offsetY = 0, + transformMatrix; + if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { + type = 'linear'; + coords = getLinearCoords(el); + } + else { + type = 'radial'; + coords = getRadialCoords(el); + } + + for (i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i], multiplier)); + } + + transformMatrix = fabric.parseTransformAttribute(gradientTransform); + + __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); + + if (gradientUnits === 'pixels') { + offsetX = -instance.left; + offsetY = -instance.top; + } + + var gradient = new fabric.Gradient({ + id: el.getAttribute('id'), + type: type, + coords: coords, + colorStops: colorStops, + gradientUnits: gradientUnits, + gradientTransform: transformMatrix, + offsetX: offsetX, + offsetY: offsetY, + }); + + return gradient; + } + /* _FROM_SVG_END_ */ + }); + + /** + * @private + */ + function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { + var propValue, finalValue; + Object.keys(options).forEach(function(prop) { + propValue = options[prop]; + if (propValue === 'Infinity') { + finalValue = 1; + } + else if (propValue === '-Infinity') { + finalValue = 0; + } + else { + finalValue = parseFloat(options[prop], 10); + if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { + finalValue *= 0.01; + if (gradientUnits === 'pixels') { + // then we need to fix those percentages here in svg parsing + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + finalValue *= svgOptions.viewBoxWidth || svgOptions.width; + } + if (prop === 'y1' || prop === 'y2') { + finalValue *= svgOptions.viewBoxHeight || svgOptions.height; + } + } + } + } + options[prop] = finalValue; + }); + } +})(); + + +(function() { + + 'use strict'; + + var toFixed = fabric.util.toFixed; + + /** + * Pattern class + * @class fabric.Pattern + * @see {@link http://fabricjs.com/patterns|Pattern demo} + * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition + */ + + + fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { + + /** + * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @type String + * @default + */ + repeat: 'repeat', + + /** + * Pattern horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX: 0, + + /** + * Pattern vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY: 0, + + /** + * crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * transform matrix to change the pattern, imported from svgs. + * @type Array + * @default + */ + patternTransform: null, + + /** + * Constructor + * @param {Object} [options] Options object + * @param {Function} [callback] function to invoke after callback init. + * @return {fabric.Pattern} thisArg + */ + initialize: function(options, callback) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img, isError) { + _this.source = img; + callback && callback(_this, isError); + }, null, this.crossOrigin); + } + }, + + /** + * Returns object representation of a pattern + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of a pattern instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + source, object; + + // element + if (typeof this.source.src === 'string') { + source = this.source.src; + } + // element + else if (typeof this.source === 'object' && this.source.toDataURL) { + source = this.source.toDataURL(); + } + + object = { + type: 'pattern', + source: source, + repeat: this.repeat, + crossOrigin: this.crossOrigin, + offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), + patternTransform: this.patternTransform ? this.patternTransform.concat() : null + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a pattern + * @param {fabric.Object} object + * @return {String} SVG representation of a pattern + */ + toSVG: function(object) { + var patternSource = typeof this.source === 'function' ? this.source() : this.source, + patternWidth = patternSource.width / object.width, + patternHeight = patternSource.height / object.height, + patternOffsetX = this.offsetX / object.width, + patternOffsetY = this.offsetY / object.height, + patternImgSrc = ''; + if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { + patternHeight = 1; + if (patternOffsetY) { + patternHeight += Math.abs(patternOffsetY); + } + } + if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { + patternWidth = 1; + if (patternOffsetX) { + patternWidth += Math.abs(patternOffsetX); + } + + } + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } + + return '\n' + + '\n' + + '\n'; + }, + /* _TO_SVG_END_ */ + + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Returns an instance of CanvasPattern + * @param {CanvasRenderingContext2D} ctx Context to create pattern + * @return {CanvasPattern} + */ + toLive: function(ctx) { + var source = this.source; + // if the image failed to load, return, and allow rest to continue loading + if (!source) { + return ''; + } + + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) { + return ''; + } + if (source.naturalWidth === 0 || source.naturalHeight === 0) { + return ''; + } + } + return ctx.createPattern(source, this.repeat); + } + }); +})(); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Shadow) { + fabric.warn('fabric.Shadow is already defined.'); + return; + } + + /** + * Shadow class + * @class fabric.Shadow + * @see {@link http://fabricjs.com/shadows|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition + */ + fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { + + /** + * Shadow color + * @type String + * @default + */ + color: 'rgb(0,0,0)', + + /** + * Shadow blur + * @type Number + */ + blur: 0, + + /** + * Shadow horizontal offset + * @type Number + * @default + */ + offsetX: 0, + + /** + * Shadow vertical offset + * @type Number + * @default + */ + offsetY: 0, + + /** + * Whether the shadow should affect stroke operations + * @type Boolean + * @default + */ + affectStroke: false, + + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `false`, the shadow will scale with the object. + * When `true`, the shadow's offsetX, offsetY, and blur will not be affected by the object's scale. + * default to false + * @type Boolean + * @default + */ + nonScaling: false, + + /** + * Constructor + * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetY properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px") + * @return {fabric.Shadow} thisArg + */ + initialize: function(options) { + + if (typeof options === 'string') { + options = this._parseShadow(options); + } + + for (var prop in options) { + this[prop] = options[prop]; + } + + this.id = fabric.Object.__uid++; + }, + + /** + * @private + * @param {String} shadow Shadow value to parse + * @return {Object} Shadow object with color, offsetX, offsetY and blur + */ + _parseShadow: function(shadow) { + var shadowStr = shadow.trim(), + offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [], + color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; + + return { + color: color.trim(), + offsetX: parseFloat(offsetsAndBlur[1], 10) || 0, + offsetY: parseFloat(offsetsAndBlur[2], 10) || 0, + blur: parseFloat(offsetsAndBlur[3], 10) || 0 + }; + }, + + /** + * Returns a string representation of an instance + * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow + * @return {String} Returns CSS3 text-shadow declaration + */ + toString: function() { + return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a shadow + * @param {fabric.Object} object + * @return {String} SVG representation of a shadow + */ + toSVG: function(object) { + var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + offset = fabric.util.rotateVector( + { x: this.offsetX, y: this.offsetY }, + fabric.util.degreesToRadians(-object.angle)), + BLUR_BOX = 20, color = new fabric.Color(this.color); + + if (object.width && object.height) { + //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion + // we add some extra space to filter box to contain the blur ( 20 ) + fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + } + if (object.flipX) { + offset.x *= -1; + } + if (object.flipY) { + offset.y *= -1; + } + + return ( + '\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\n' + + '\n'); + }, + /* _TO_SVG_END_ */ + + /** + * Returns object representation of a shadow + * @return {Object} Object representation of a shadow instance + */ + toObject: function() { + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY, + affectStroke: this.affectStroke, + nonScaling: this.nonScaling + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + + ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke', 'nonScaling'].forEach(function(prop) { + if (this[prop] !== proto[prop]) { + obj[prop] = this[prop]; + } + }, this); + + return obj; + } + }); + + /** + * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") + * @static + * @field + * @memberOf fabric.Shadow + */ + // eslint-disable-next-line max-len + fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function () { + + 'use strict'; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + toFixed = fabric.util.toFixed, + transformPoint = fabric.util.transformPoint, + invertTransform = fabric.util.invertTransform, + getNodeCanvas = fabric.util.getNodeCanvas, + createCanvasElement = fabric.util.createCanvasElement, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * Static canvas class + * @class fabric.StaticCanvas + * @mixes fabric.Collection + * @mixes fabric.Observable + * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition + * @fires before:render + * @fires after:render + * @fires canvas:cleared + * @fires object:added + * @fires object:removed + */ + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + }, + + /** + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} + * @default + */ + backgroundColor: '', + + /** + * Background image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as background, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + backgroundImage: null, + + /** + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} + * @default + */ + overlayColor: '', + + /** + * Overlay image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as overlay, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + overlayImage: null, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * if set to false, takes precedence over the object value. + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @type Boolean + * @default + */ + stateful: false, + + /** + * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove}, + * {@link fabric.StaticCanvas.moveTo}, {@link fabric.StaticCanvas.clear} and many more, should also re-render canvas. + * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once + * since the renders are quequed and executed one per frame. + * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) + * Left default to true to do not break documentation and old app, fiddles. + * @type Boolean + * @default + */ + renderOnAddRemove: true, + + /** + * Indicates whether object controls (borders/controls) are rendered above overlay image + * @type Boolean + * @default + */ + controlsAboveOverlay: false, + + /** + * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas + * @type Boolean + * @default + */ + allowTouchScrolling: false, + + /** + * Indicates whether this canvas will use image smoothing, this is on by default in browsers + * @type Boolean + * @default + */ + imageSmoothingEnabled: true, + + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: fabric.iMatrix.concat(), + + /** + * if set to false background image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + backgroundVpt: true, + + /** + * if set to false overlya image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + overlayVpt: true, + + /** + * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + * @type Boolean + * @default + */ + enableRetinaScaling: true, + + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + * @memberOf fabric.StaticCanvas.prototype + */ + vptCoords: { }, + + /** + * Based on vptCoords and object.aCoords, skip rendering of objects that + * are not included in current viewport. + * May greatly help in applications with crowded canvas and use of zoom/pan + * If One of the corner of the bounding box of the object is on the canvas + * the objects get rendered. + * @memberOf fabric.StaticCanvas.prototype + * @type Boolean + * @default + */ + skipOffscreen: true, + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the canvas has rendered, and the context is placed in the + * top left corner of the canvas. + * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true + * @type fabric.Object + */ + clipPath: undefined, + + /** + * @private + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic: function(el, options) { + var cb = this.requestRenderAllBound; + this._objects = []; + this._createLowerCanvas(el); + this._initOptions(options); + // only initialize retina scaling once + if (!this.interactive) { + this._initRetinaScaling(); + } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } + this.calcOffset(); + }, + + /** + * @private + */ + _isRetinaScaling: function() { + return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling); + }, + + /** + * @private + * @return {Number} retinaScaling if applied, otherwise 1; + */ + getRetinaScaling: function() { + return this._isRetinaScaling() ? fabric.devicePixelRatio : 1; + }, + + /** + * @private + */ + _initRetinaScaling: function() { + if (!this._isRetinaScaling()) { + return; + } + var scaleRatio = fabric.devicePixelRatio; + this.__initRetinaScaling(scaleRatio, this.lowerCanvasEl, this.contextContainer); + if (this.upperCanvasEl) { + this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop); + } + }, + + __initRetinaScaling: function(scaleRatio, canvas, context) { + canvas.setAttribute('width', this.width * scaleRatio); + canvas.setAttribute('height', this.height * scaleRatio); + context.scale(scaleRatio, scaleRatio); + }, + + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + // TODO: fix stretched examples + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to + * @param {Function} callback Callback to invoke when foreground color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img, isError) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img, isError); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + image && (image.canvas = this); + callback && callback(image, false); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + + /** + * @private + */ + _createCanvasElement: function() { + var element = createCanvasElement(); + if (!element) { + throw CANVAS_INIT_ERROR; + } + if (!element.style) { + element.style = { }; + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + return element; + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions: function (options) { + var lowerCanvasEl = this.lowerCanvasEl; + this._setOptions(options); + + this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; + this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) { + return; + } + + lowerCanvasEl.width = this.width; + lowerCanvasEl.height = this.height; + + lowerCanvasEl.style.width = this.width + 'px'; + lowerCanvasEl.style.height = this.height + 'px'; + + this.viewportTransform = this.viewportTransform.slice(); + }, + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas: function (canvasEl) { + // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node + if (canvasEl && canvasEl.getContext) { + this.lowerCanvasEl = canvasEl; + } + else { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + } + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + this._originalCanvasStyle = this.lowerCanvasEl.style; + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width (in px) + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height (in px) + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @param {Number|String} value Value to set width to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value, options) { + return this.setDimensions({ width: value }, options); + }, + + /** + * Sets height of this canvas instance + * @param {Number|String} value Value to set height to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value, options) { + return this.setDimensions({ height: value }, options); + }, + + /** + * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) + * @param {Object} dimensions Object with width/height properties + * @param {Number|String} [dimensions.width] Width of canvas element + * @param {Number|String} [dimensions.height] Height of canvas element + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function (dimensions, options) { + var cssValue; + + options = options || {}; + + for (var prop in dimensions) { + cssValue = dimensions[prop]; + + if (!options.cssOnly) { + this._setBackstoreDimension(prop, dimensions[prop]); + cssValue += 'px'; + this.hasLostContext = true; + } + + if (!options.backstoreOnly) { + this._setCssDimension(prop, cssValue); + } + } + if (this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(); + } + this._initRetinaScaling(); + this.calcOffset(); + + if (!options.cssOnly) { + this.requestRenderAll(); + } + + return this; + }, + + /** + * Helper for setting width/height + * @private + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setBackstoreDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + this[prop] = value; + + return this; + }, + + /** + * Helper for setting css width/height + * @private + * @param {String} prop property (width|height) + * @param {String} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setCssDimension: function (prop, value) { + this.lowerCanvasEl.style[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl.style[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value; + } + + return this; + }, + + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return this.viewportTransform[0]; + }, + + /** + * Sets viewport transform of this canvas instance + * @param {Array} vpt the transform in the form of context.transform + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform: function (vpt) { + var activeObject = this._activeObject, + backgroundObject = this.backgroundImage, + overlayObject = this.overlayImage, + object, i, len; + this.viewportTransform = vpt; + for (i = 0, len = this._objects.length; i < len; i++) { + object = this._objects[i]; + object.group || object.setCoords(true); + } + if (activeObject) { + activeObject.setCoords(); + } + if (backgroundObject) { + backgroundObject.setCoords(true); + } + if (overlayObject) { + overlayObject.setCoords(true); + } + this.calcViewportBoundaries(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Sets zoom level of this canvas instance, the zoom centered around point + * meaning that following zoom to point with the same point will have the visual + * effect of the zoom originating from that point. The point won't move. + * It has nothing to do with canvas center or visual center of the viewport. + * @param {fabric.Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint: function (point, value) { + // TODO: just change the scale, preserve other transformations + var before = point, vpt = this.viewportTransform.slice(0); + point = transformPoint(point, invertTransform(this.viewportTransform)); + vpt[0] = value; + vpt[3] = value; + var after = transformPoint(point, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + this.zoomToPoint(new fabric.Point(0, 0), value); + return this; + }, + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {fabric.Point} point to move to + * @return {fabric.Canvas} instance + * @chainable true + */ + absolutePan: function (point) { + var vpt = this.viewportTransform.slice(0); + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); + }, + + /** + * Pans viewpoint relatively + * @param {fabric.Point} point (position vector) to move by + * @return {fabric.Canvas} instance + * @chainable true + */ + relativePan: function (point) { + return this.absolutePan(new fabric.Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + )); + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + this.fire('object:removed', { target: obj }); + obj.fire('removed'); + delete obj.canvas; + }, + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this.remove.apply(this, this.getObjects()); + this.backgroundImage = null; + this.overlayImage = null; + this.backgroundColor = ''; + this.overlayColor = ''; + if (this._hasITextHandlers) { + this.off('mouse:up', this._mouseUpITextHandler); + this._iTextInstances = null; + this._hasITextHandlers = false; + } + this.clearContext(this.contextContainer); + this.fire('canvas:cleared'); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Renders the canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._objects); + + return this; + }, + + /** + * Function created to be instance bound at initialization + * used in requestAnimationFrame rendering + * Let the fabricJS call it. If you call it manually you could have more + * animationFrame stacking on to of each other + * for an imperative rendering, use canvas.renderAll + * @private + * @return {fabric.Canvas} instance + * @chainable + */ + renderAndReset: function() { + this.isRendering = 0; + this.renderAll(); + }, + + /** + * Append a renderAll request to next animation frame. + * unless one is already in progress, in that case nothing is done + * a boolean flag will avoid appending more. + * @return {fabric.Canvas} instance + * @chainable + */ + requestRenderAll: function () { + if (!this.isRendering) { + this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound); + } + return this; + }, + + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries: function() { + var points = { }, width = this.width, height = this.height, + iVpt = invertTransform(this.viewportTransform); + points.tl = transformPoint({ x: 0, y: 0 }, iVpt); + points.br = transformPoint({ x: width, y: height }, iVpt); + points.tr = new fabric.Point(points.br.x, points.tl.y); + points.bl = new fabric.Point(points.tl.x, points.br.y); + this.vptCoords = points; + return points; + }, + + cancelRequestedRender: function() { + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + }, + + /** + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render + * @return {fabric.Canvas} instance + * @chainable + */ + renderCanvas: function(ctx, objects) { + var v = this.viewportTransform, path = this.clipPath; + this.cancelRequestedRender(); + this.calcViewportBoundaries(); + this.clearContext(ctx); + fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled); + this.fire('before:render', { ctx: ctx, }); + this._renderBackground(ctx); + + ctx.save(); + //apply viewport transform once for all rendering process + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._renderObjects(ctx, objects); + ctx.restore(); + if (!this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + if (path) { + path.canvas = this; + // needed to setup a couple of variables + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCanvas(ctx); + } + this._renderOverlay(ctx); + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + this.fire('after:render', { ctx: ctx, }); + }, + + /** + * Paint the cached clipPath on the lowerCanvasEl + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCanvas: function(ctx) { + var v = this.viewportTransform, path = this.clipPath; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4; + ctx.globalCompositeOperation = 'destination-in'; + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} objects to render + */ + _renderObjects: function(ctx, objects) { + var i, len; + for (i = 0, len = objects.length; i < len; ++i) { + objects[i] && objects[i].render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {string} property 'background' or 'overlay' + */ + _renderBackgroundOrOverlay: function(ctx, property) { + var fill = this[property + 'Color'], object = this[property + 'Image'], + v = this.viewportTransform, needsVpt = this[property + 'Vpt']; + if (!fill && !object) { + return; + } + if (fill) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(this.width, 0); + ctx.lineTo(this.width, this.height); + ctx.lineTo(0, this.height); + ctx.closePath(); + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx, this) + : fill; + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); + var m = fill.gradientTransform || fill.patternTransform; + m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + ctx.fill(); + ctx.restore(); + } + if (object) { + ctx.save(); + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + object.render(ctx); + ctx.restore(); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'background'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'overlay'); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.height / 2, + left: this.width / 2 + }; + }, + + /** + * Centers object horizontally in the canvas + * @param {fabric.Object} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + }, + + /** + * Centers object vertically in the canvas + * @param {fabric.Object} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + }, + + /** + * Centers object vertically and horizontally in the canvas + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function(object) { + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); + }, + + /** + * Centers object vertically and horizontally in the viewport + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObject: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, vpCenter); + }, + + /** + * Centers object horizontally in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectH: function(object) { + var vpCenter = this.getVpCenter(); + this._centerObject(object, new fabric.Point(vpCenter.x, object.getCenterPoint().y)); + return this; + }, + + /** + * Centers object Vertically in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectV: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, vpCenter.y)); + }, + + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {fabric.Point} vpCenter, viewport center + * @chainable + */ + getVpCenter: function() { + var center = this.getCenter(), + iVpt = invertTransform(this.viewportTransform); + return transformPoint({ x: center.left, y: center.top }, iVpt); + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Returns dataless JSON representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} json string + */ + toDatalessJSON: function (propertiesToInclude) { + return this.toDatalessObject(propertiesToInclude); + }, + + /** + * Returns object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function (propertiesToInclude) { + return this._toObjectMethod('toObject', propertiesToInclude); + }, + + /** + * Returns dataless object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function (propertiesToInclude) { + return this._toObjectMethod('toDatalessObject', propertiesToInclude); + }, + + /** + * @private + */ + _toObjectMethod: function (methodName, propertiesToInclude) { + + var clipPath = this.clipPath, data = { + version: fabric.version, + objects: this._toObjects(methodName, propertiesToInclude), + }; + if (clipPath && !clipPath.excludeFromExport) { + data.clipPath = this._toObject(this.clipPath, methodName, propertiesToInclude); + } + extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); + + fabric.util.populateWithProperties(this, data, propertiesToInclude); + + return data; + }, + + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this._objects.filter(function(object) { + return !object.excludeFromExport; + }).map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function(methodName, propertiesToInclude) { + var data = {}, bgImage = this.backgroundImage, overlayImage = this.overlayImage, + bgColor = this.backgroundColor, overlayColor = this.overlayColor; + + if (bgColor && bgColor.toObject) { + if (!bgColor.excludeFromExport) { + data.background = bgColor.toObject(propertiesToInclude); + } + } + else if (bgColor) { + data.background = bgColor; + } + + if (overlayColor && overlayColor.toObject) { + if (!overlayColor.excludeFromExport) { + data.overlay = overlayColor.toObject(propertiesToInclude); + } + } + else if (overlayColor) { + data.overlay = overlayColor; + } + + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject(bgImage, methodName, propertiesToInclude); + } + if (overlayImage && !overlayImage.excludeFromExport) { + data.overlayImage = this._toObject(overlayImage, methodName, propertiesToInclude); + } + + return data; + }, + + /* _TO_SVG_START_ */ + /** + * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, + * a zoomed canvas will then produce zoomed SVG output. + * @type Boolean + * @default + */ + svgViewportTransformation: true, + + /** + * Returns SVG representation of canvas + * @function + * @param {Object} [options] Options object for SVG output + * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included + * @param {Object} [options.viewBox] SVG viewbox object + * @param {Number} [options.viewBox.x] x-coordinate of viewbox + * @param {Number} [options.viewBox.y] y-coordinate of viewbox + * @param {Number} [options.viewBox.width] Width of viewbox + * @param {Number} [options.viewBox.height] Height of viewbox + * @param {String} [options.encoding=UTF-8] Encoding of SVG output + * @param {String} [options.width] desired width of svg with or without units + * @param {String} [options.height] desired height of svg with or without units + * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. + * @return {String} SVG string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} + * @example Normal SVG output + * var svg = canvas.toSVG(); + * @example SVG output without preamble (without <?xml ../>) + * var svg = canvas.toSVG({suppressPreamble: true}); + * @example SVG output with viewBox attribute + * var svg = canvas.toSVG({ + * viewBox: { + * x: 100, + * y: 100, + * width: 200, + * height: 300 + * } + * }); + * @example SVG output with different encoding (default: UTF-8) + * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); + * @example Modify SVG output with reviver function + * var svg = canvas.toSVG(null, function(svg) { + * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); + * }); + */ + toSVG: function(options, reviver) { + options || (options = { }); + options.reviver = reviver; + var markup = []; + + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'background'); + this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); + this._setSVGObjects(markup, reviver); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'overlay'); + this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { + var width = options.width || this.width, + height = options.height || this.height, + vpt, viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + if (options.viewBox) { + viewBox = 'viewBox="' + + options.viewBox.x + ' ' + + options.viewBox.y + ' ' + + options.viewBox.width + ' ' + + options.viewBox.height + '" '; + } + else { + if (this.svgViewportTransformation) { + vpt = this.viewportTransform; + viewBox = 'viewBox="' + + toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + '" '; + } + } + + markup.push( + '\n', + 'Created with Fabric.js ', fabric.version, '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + this.createSVGClipPathMarkup(options), + '\n' + ); + }, + + createSVGClipPathMarkup: function(options) { + var clipPath = this.clipPath; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + return '\n' + + this.clipPath.toClipPathSVG(options.reviver) + + '\n'; + } + return ''; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup: function() { + var _this = this, + markup = ['background', 'overlay'].map(function(prop) { + var fill = _this[prop + 'Color']; + if (fill && fill.toLive) { + var shouldTransform = _this[prop + 'Vpt'], vpt = _this.viewportTransform, + object = { + width: _this.width / (shouldTransform ? vpt[0] : 1), + height: _this.height / (shouldTransform ? vpt[3] : 1) + }; + return fill.toSVG( + object, + { additionalTransform: shouldTransform ? fabric.util.matrixToSVG(vpt) : '' } + ); + } + }); + return markup.join(''); + }, + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function() { + var markup = '', fontList = { }, obj, fontFamily, + style, row, rowIndex, _char, charIndex, i, len, + fontPaths = fabric.fontPaths, objects = []; + + this._objects.forEach(function add(object) { + objects.push(object); + if (object._objects) { + object._objects.forEach(add); + } + }); + + for (i = 0, len = objects.length; i < len; i++) { + obj = objects[i]; + fontFamily = obj.fontFamily; + if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { + continue; + } + fontList[fontFamily] = true; + if (!obj.styles) { + continue; + } + style = obj.styles; + for (rowIndex in style) { + row = style[rowIndex]; + for (charIndex in row) { + _char = row[charIndex]; + fontFamily = _char.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; + } + } + } + } + + for (var j in fontList) { + markup += [ + '\t\t@font-face {\n', + '\t\t\tfont-family: \'', j, '\';\n', + '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', + '\t\t}\n' + ].join(''); + } + + if (markup) { + markup = [ + '\t\n' + ].join(''); + } + + return markup; + }, + + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { + var instance, i, len, objects = this._objects; + for (i = 0, len = objects.length; i < len; i++) { + instance = objects[i]; + if (instance.excludeFromExport) { + continue; + } + this._setSVGObject(markup, instance, reviver); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + markup.push(instance.toSVG(reviver)); + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property, reviver) { + if (this[property] && !this[property].excludeFromExport && this[property].toSVG) { + markup.push(this[property].toSVG(reviver)); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + var filler = this[property + 'Color'], vpt = this.viewportTransform, finalWidth = this.width, + finalHeight = this.height; + if (!filler) { + return; + } + if (filler.toLive) { + var repeat = filler.repeat, iVpt = fabric.util.invertTransform(vpt), shouldInvert = this[property + 'Vpt'], + additionalTransform = shouldInvert ? fabric.util.matrixToSVG(iVpt) : ''; + markup.push( + '\n' + ); + } + else { + markup.push( + '\n' + ); + } + }, + /* _TO_SVG_END_ */ + + /** + * Moves an object or the objects of a multiple selection + * to the bottom of the stack of drawn objects + * @param {fabric.Object} object Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.unshift(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.unshift(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or the objects of a multiple selection + * to the top of the stack of drawn objects + * @param {fabric.Object} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.push(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.push(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or a selection down in stack of drawn objects + * An optional parameter, intersecting allows to move the object in behind + * the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx > 0 + objsMoved) { + newIdx = idx - 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== 0) { + // if object is not on the bottom of stack + newIdx = this._findNewLowerIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx, i; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (i = idx - 1; i >= 0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + + /** + * Moves an object or a selection up in stack of drawn objects + * An optional parameter, intersecting allows to move the object in front + * of the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx < this._objects.length - 1 - objsMoved) { + newIdx = idx + 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== this._objects.length - 1) { + // if object is not on top of stack (last item in an array) + newIdx = this._findNewUpperIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx, i, len; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (i = idx + 1, len = this._objects.length; i < len; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx + 1; + } + + return newIdx; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {fabric.Object} object Object to send + * @param {Number} index Position to move to + * @return {fabric.Canvas} thisArg + * @chainable + */ + moveTo: function (object, index) { + removeFromArray(this._objects, object); + this._objects.splice(index, 0, object); + return this.renderOnAddRemove && this.requestRenderAll(); + }, + + /** + * Clears a canvas element and dispose objects + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + // cancel eventually ongoing renders + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + this.forEachObject(function(object) { + object.dispose && object.dispose(); + }); + this._objects = []; + if (this.backgroundImage && this.backgroundImage.dispose) { + this.backgroundImage.dispose(); + } + this.backgroundImage = null; + if (this.overlayImage && this.overlayImage.dispose) { + this.overlayImage.dispose(); + } + this.overlayImage = null; + this._iTextInstances = null; + this.contextContainer = null; + // restore canvas style + this.lowerCanvasEl.classList.remove('lower-canvas'); + this.lowerCanvasEl.style = this._originalCanvasStyle; + delete this._originalCanvasStyle; + // restore canvas size to original size in case retina scaling was applied + this.lowerCanvasEl.setAttribute('width', this.width); + this.lowerCanvasEl.setAttribute('height', this.height); + fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); + this.lowerCanvasEl = undefined; + return this; + }, + + /** + * Returns a string representation of an instance + * @return {String} string representation of an instance + */ + toString: function () { + return '#'; + } + }); + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + extend(fabric.StaticCanvas.prototype, fabric.Collection); + extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); + + extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { + + /** + * @static + * @type String + * @default + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @param {String} methodName Method to check support for; + * Could be one of "setLineDash" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = createCanvasElement(); + + if (!el || !el.getContext) { + return null; + } + + var ctx = el.getContext('2d'); + if (!ctx) { + return null; + } + + switch (methodName) { + + case 'setLineDash': + return typeof ctx.setLineDash !== 'undefined'; + + default: + return null; + } + } + }); + + /** + * Returns Object representation of canvas + * this alias is provided because if you call JSON.stringify on an instance, + * the toJSON object will be invoked if it exists. + * Having a toJSON method means you can do JSON.stringify(myCanvas) + * @function + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON compatible object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} + * @example JSON without additional properties + * var json = canvas.toJSON(); + * @example JSON with additional properties included + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); + * @example JSON without default values + * canvas.includeDefaultValues = false; + * var json = canvas.toJSON(); + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + + if (fabric.isLikelyNode) { + fabric.StaticCanvas.prototype.createPNGStream = function() { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createPNGStream(); + }; + fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createJPEGStream(opts); + }; + } +})(); + + +/** + * BaseBrush class + * @class fabric.BaseBrush + * @see {@link http://fabricjs.com/freedrawing|Freedrawing demo} + */ +fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ { + + /** + * Color of a brush + * @type String + * @default + */ + color: 'rgb(0, 0, 0)', + + /** + * Width of a brush, has to be a Number, no string literals + * @type Number + * @default + */ + width: 1, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), + * "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Line endings style of a brush (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'round', + + /** + * Corner style of a brush (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'round', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of a brush's + * @type Number + * @default + */ + strokeMiterLimit: 10, + + /** + * Stroke Dash Array. + * @type Array + * @default + */ + strokeDashArray: null, + + /** + * When `true`, the free drawing is limited to the whiteboard size. Default to false. + * @type Boolean + * @default false + */ + + limitedToCanvasSize: false, + + + /** + * Sets brush styles + * @private + */ + _setBrushStyles: function() { + var ctx = this.canvas.contextTop; + ctx.strokeStyle = this.color; + ctx.lineWidth = this.width; + ctx.lineCap = this.strokeLineCap; + ctx.miterLimit = this.strokeMiterLimit; + ctx.lineJoin = this.strokeLineJoin; + ctx.setLineDash(this.strokeDashArray || []); + }, + + /** + * Sets the transformation on given context + * @param {RenderingContext2d} ctx context to render on + * @private + */ + _saveAndTransform: function(ctx) { + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + }, + + /** + * Sets brush shadow styles + * @private + */ + _setShadow: function() { + if (!this.shadow) { + return; + } + + var canvas = this.canvas, + shadow = this.shadow, + ctx = canvas.contextTop, + zoom = canvas.getZoom(); + if (canvas && canvas._isRetinaScaling()) { + zoom *= fabric.devicePixelRatio; + } + + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * zoom; + ctx.shadowOffsetX = shadow.offsetX * zoom; + ctx.shadowOffsetY = shadow.offsetY * zoom; + }, + + needsFullRender: function() { + var color = new fabric.Color(this.color); + return color.getAlpha() < 1 || !!this.shadow; + }, + + /** + * Removes brush shadow styles + * @private + */ + _resetShadow: function() { + var ctx = this.canvas.contextTop; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * Check is pointer is outside canvas boundaries + * @param {Object} pointer + * @private + */ + _isOutSideCanvas: function(pointer) { + return pointer.x < 0 || pointer.x > this.canvas.getWidth() || pointer.y < 0 || pointer.y > this.canvas.getHeight(); + } +}); + + +(function() { + /** + * PencilBrush class + * @class fabric.PencilBrush + * @extends fabric.BaseBrush + */ + fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { + + /** + * Discard points that are less than `decimate` pixel distant from each other + * @type Number + * @default 0.4 + */ + decimate: 0.4, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.PencilBrush} Instance of a pencil brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this._points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + _drawSegment: function (ctx, p1, p2) { + var midPoint = p1.midPointFrom(p2); + ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); + return midPoint; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this._prepareForDrawing(pointer); + // capture coordinates immediately + // this allows to draw dots (when movement never occurs) + this._captureDrawingPath(pointer); + this._render(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + if (this._captureDrawingPath(pointer) && this._points.length > 1) { + if (this.needsFullRender()) { + // redraw curve + // clear top canvas + this.canvas.clearContext(this.canvas.contextTop); + this._render(); + } + else { + var points = this._points, length = points.length, ctx = this.canvas.contextTop; + // draw the curve update + this._saveAndTransform(ctx); + if (this.oldEnd) { + ctx.beginPath(); + ctx.moveTo(this.oldEnd.x, this.oldEnd.y); + } + this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); + ctx.stroke(); + ctx.restore(); + } + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function(options) { + if (!this.canvas._isMainEvent(options.e)) { + return true; + } + this.oldEnd = undefined; + this._finalizeAndAddPath(); + return false; + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _prepareForDrawing: function(pointer) { + + var p = new fabric.Point(pointer.x, pointer.y); + + this._reset(); + this._addPoint(p); + this.canvas.contextTop.moveTo(p.x, p.y); + }, + + /** + * @private + * @param {fabric.Point} point Point to be added to points array + */ + _addPoint: function(point) { + if (this._points.length > 1 && point.eq(this._points[this._points.length - 1])) { + return false; + } + this._points.push(point); + return true; + }, + + /** + * Clear points array and set contextTop canvas style. + * @private + */ + _reset: function() { + this._points = []; + this._setBrushStyles(); + this._setShadow(); + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _captureDrawingPath: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + return this._addPoint(pointerPoint); + }, + + /** + * Draw a smooth path on the topCanvas using quadraticCurveTo + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + p1 = this._points[0], + p2 = this._points[1]; + + this._saveAndTransform(ctx); + ctx.beginPath(); + //if we only have 2 points in the path and they are the same + //it means that the user only clicked the canvas without moving the mouse + //then we should be drawing a dot. A path isn't drawn between two identical dots + //that's why we set them apart a bit + if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { + var width = this.width / 1000; + p1 = new fabric.Point(p1.x, p1.y); + p2 = new fabric.Point(p2.x, p2.y); + p1.x -= width; + p2.x += width; + } + ctx.moveTo(p1.x, p1.y); + + for (i = 1, len = this._points.length; i < len; i++) { + // we pick the point between pi + 1 & pi + 2 as the + // end point and p1 as our control point. + this._drawSegment(ctx, p1, p2); + p1 = this._points[i]; + p2 = this._points[i + 1]; + } + // Draw last line as a straight line while + // we wait for the next point to be able to calculate + // the bezier control point + ctx.lineTo(p1.x, p1.y); + ctx.stroke(); + ctx.restore(); + }, + + /** + * Converts points to SVG path + * @param {Array} points Array of points + * @return {(string|number)[][]} SVG path commands + */ + convertPointsToSVGPath: function (points) { + var correction = this.width / 1000; + return fabric.util.getSmoothPathFromPoints(points, correction); + }, + + /** + * @private + * @param {(string|number)[][]} pathData SVG path commands + * @returns {boolean} + */ + _isEmptySVGPath: function (pathData) { + var pathString = fabric.util.joinPath(pathData); + return pathString === 'M 0 0 Q 0 0 0 0 L 0 0'; + }, + + /** + * Creates fabric.Path object to add on canvas + * @param {(string|number)[][]} pathData Path data + * @return {fabric.Path} Path to add on canvas + */ + createPath: function(pathData) { + var path = new fabric.Path(pathData, { + fill: null, + stroke: this.color, + strokeWidth: this.width, + strokeLineCap: this.strokeLineCap, + strokeMiterLimit: this.strokeMiterLimit, + strokeLineJoin: this.strokeLineJoin, + strokeDashArray: this.strokeDashArray, + }); + if (this.shadow) { + this.shadow.affectStroke = true; + path.shadow = new fabric.Shadow(this.shadow); + } + + return path; + }, + + /** + * Decimate points array with the decimate value + */ + decimatePoints: function(points, distance) { + if (points.length <= 2) { + return points; + } + var zoom = this.canvas.getZoom(), adjustedDistance = Math.pow(distance / zoom, 2), + i, l = points.length - 1, lastPoint = points[0], newPoints = [lastPoint], + cDistance; + for (i = 1; i < l - 1; i++) { + cDistance = Math.pow(lastPoint.x - points[i].x, 2) + Math.pow(lastPoint.y - points[i].y, 2); + if (cDistance >= adjustedDistance) { + lastPoint = points[i]; + newPoints.push(lastPoint); + } + } + /** + * Add the last point from the original line to the end of the array. + * This ensures decimate doesn't delete the last point on the line, and ensures the line is > 1 point. + */ + newPoints.push(points[l]); + return newPoints; + }, + + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. + */ + _finalizeAndAddPath: function() { + var ctx = this.canvas.contextTop; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points); + if (this._isEmptySVGPath(pathData)) { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.requestRenderAll(); + return; + } + + var path = this.createPath(pathData); + this.canvas.clearContext(this.canvas.contextTop); + this.canvas.fire('before:path:created', { path: path }); + this.canvas.add(path); + this.canvas.requestRenderAll(); + path.setCoords(); + this._resetShadow(); + + + // fire event 'path' created + this.canvas.fire('path:created', { path: path }); + } + }); +})(); + + +/** + * CircleBrush class + * @class fabric.CircleBrush + */ +fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { + + /** + * Width of a brush + * @type Number + * @default + */ + width: 10, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.CircleBrush} Instance of a circle brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + drawDot: function(pointer) { + var point = this.addPoint(pointer), + ctx = this.canvas.contextTop; + this._saveAndTransform(ctx); + this.dot(ctx, point); + ctx.restore(); + }, + + dot: function(ctx, point) { + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + }, + + /** + * Invoked on mouse down + */ + onMouseDown: function(pointer) { + this.points.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + this.drawDot(pointer); + }, + + /** + * Render the full state of the brush + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + points = this.points; + this._saveAndTransform(ctx); + for (i = 0, len = points.length; i < len; i++) { + this.dot(ctx, points[i]); + } + ctx.restore(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + if (this.needsFullRender()) { + this.canvas.clearContext(this.canvas.contextTop); + this.addPoint(pointer); + this._render(); + } + else { + this.drawDot(pointer); + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove, i, len; + this.canvas.renderOnAddRemove = false; + + var circles = []; + + for (i = 0, len = this.points.length; i < len; i++) { + var point = this.points[i], + circle = new fabric.Circle({ + radius: point.radius, + left: point.x, + top: point.y, + originX: 'center', + originY: 'center', + fill: point.fill + }); + + this.shadow && (circle.shadow = new fabric.Shadow(this.shadow)); + + circles.push(circle); + } + var group = new fabric.Group(circles); + group.canvas = this.canvas; + + this.canvas.fire('before:path:created', { path: group }); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @param {Object} pointer + * @return {fabric.Point} Just added pointer point + */ + addPoint: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y), + + circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2, + + circleColor = new fabric.Color(this.color) + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); + + pointerPoint.radius = circleRadius; + pointerPoint.fill = circleColor; + + this.points.push(pointerPoint); + + return pointerPoint; + } +}); + + +/** + * SprayBrush class + * @class fabric.SprayBrush + */ +fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { + + /** + * Width of a spray + * @type Number + * @default + */ + width: 10, + + /** + * Density of a spray (number of dots per chunk) + * @type Number + * @default + */ + density: 20, + + /** + * Width of spray dots + * @type Number + * @default + */ + dotWidth: 1, + + /** + * Width variance of spray dots + * @type Number + * @default + */ + dotWidthVariance: 1, + + /** + * Whether opacity of a dot should be random + * @type Boolean + * @default + */ + randomOpacity: false, + + /** + * Whether overlapping dots (rectangles) should be removed (for performance reasons) + * @type Boolean + * @default + */ + optimizeOverlapping: true, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.SprayBrush} Instance of a spray brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.sprayChunks = []; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer) { + this.sprayChunks.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + var rects = []; + + for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + var sprayChunk = this.sprayChunks[i]; + + for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) { + + var rect = new fabric.Rect({ + width: sprayChunk[j].width, + height: sprayChunk[j].width, + left: sprayChunk[j].x + 1, + top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', + fill: this.color + }); + rects.push(rect); + } + } + + if (this.optimizeOverlapping) { + rects = this._getOptimizedRects(rects); + } + + var group = new fabric.Group(rects); + this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); + this.canvas.fire('before:path:created', { path: group }); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @private + * @param {Array} rects + */ + _getOptimizedRects: function(rects) { + + // avoid creating duplicate rects at the same coordinates + var uniqueRects = { }, key, i, len; + + for (i = 0, len = rects.length; i < len; i++) { + key = rects[i].left + '' + rects[i].top; + if (!uniqueRects[key]) { + uniqueRects[key] = rects[i]; + } + } + var uniqueRectsArray = []; + for (key in uniqueRects) { + uniqueRectsArray.push(uniqueRects[key]); + } + + return uniqueRectsArray; + }, + + /** + * Render new chunk of spray brush + */ + render: function(sprayChunk) { + var ctx = this.canvas.contextTop, i, len; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, len = sprayChunk.length; i < len; i++) { + var point = sprayChunk[i]; + if (typeof point.opacity !== 'undefined') { + ctx.globalAlpha = point.opacity; + } + ctx.fillRect(point.x, point.y, point.width, point.width); + } + ctx.restore(); + }, + + /** + * Render all spray chunks + */ + _render: function() { + var ctx = this.canvas.contextTop, i, ilen; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + this.render(this.sprayChunks[i]); + } + ctx.restore(); + }, + + /** + * @param {Object} pointer + */ + addSprayChunk: function(pointer) { + this.sprayChunkPoints = []; + + var x, y, width, radius = this.width / 2, i; + + for (i = 0; i < this.density; i++) { + + x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); + y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); + + if (this.dotWidthVariance) { + width = fabric.util.getRandomInt( + // bottom clamp width to 1 + Math.max(1, this.dotWidth - this.dotWidthVariance), + this.dotWidth + this.dotWidthVariance); + } + else { + width = this.dotWidth; + } + + var point = new fabric.Point(x, y); + point.width = width; + + if (this.randomOpacity) { + point.opacity = fabric.util.getRandomInt(0, 100) / 100; + } + + this.sprayChunkPoints.push(point); + } + + this.sprayChunks.push(this.sprayChunkPoints); + } +}); + + +/** + * PatternBrush class + * @class fabric.PatternBrush + * @extends fabric.BaseBrush + */ +fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { + + getPatternSrc: function() { + + var dotWidth = 20, + dotDistance = 5, + patternCanvas = fabric.util.createCanvasElement(), + patternCtx = patternCanvas.getContext('2d'); + + patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; + + patternCtx.fillStyle = this.color; + patternCtx.beginPath(); + patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); + patternCtx.closePath(); + patternCtx.fill(); + + return patternCanvas; + }, + + getPatternSrcFunction: function() { + return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); + }, + + /** + * Creates "pattern" instance property + */ + getPattern: function() { + return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); + }, + + /** + * Sets brush styles + */ + _setBrushStyles: function() { + this.callSuper('_setBrushStyles'); + this.canvas.contextTop.strokeStyle = this.getPattern(); + }, + + /** + * Creates path + */ + createPath: function(pathData) { + var path = this.callSuper('createPath', pathData), + topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); + + path.stroke = new fabric.Pattern({ + source: this.source || this.getPatternSrcFunction(), + offsetX: -topLeft.x, + offsetY: -topLeft.y + }); + return path; + } +}); + + +(function() { + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + isTouchEvent = fabric.util.isTouchEvent; + + /** + * Canvas class + * @class fabric.Canvas + * @extends fabric.StaticCanvas + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition + * + * @fires object:modified at the end of a transform or any change when statefull is true + * @fires object:rotating while an object is being rotated from the control + * @fires object:scaling while an object is being scaled by controls + * @fires object:moving while an object is being dragged + * @fires object:skewing while an object is being skewed from the controls + * + * @fires before:transform before a transform is is started + * @fires before:selection:cleared + * @fires selection:cleared + * @fires selection:updated + * @fires selection:created + * + * @fires path:created after a drawing operation ends and the path is added + * @fires mouse:down + * @fires mouse:move + * @fires mouse:up + * @fires mouse:down:before on mouse down, before the inner fabric logic runs + * @fires mouse:move:before on mouse move, before the inner fabric logic runs + * @fires mouse:up:before on mouse up, before the inner fabric logic runs + * @fires mouse:over + * @fires mouse:out + * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + * @fires after:render at the end of the render process, receives the context in the callback + * @fires before:render at start the render process, receives the context in the callback + * + * the following events are deprecated: + * @fires object:rotated at the end of a rotation transform + * @fires object:scaled at the end of a scale transform + * @fires object:moved at the end of translation transform + * @fires object:skewed at the end of a skew transform + */ + fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + }, + + /** + * When true, objects can be transformed by one side (unproportionally) + * when dragged on the corners that normally would not do that. + * @type Boolean + * @default + * @since fabric 4.0 // changed name and default value + */ + uniformScaling: true, + + /** + * Indicates which key switches uniform scaling. + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * totally wrong named. this sounds like `uniform scaling` + * if Canvas.uniformScaling is true, pressing this will set it to false + * and viceversa. + * @since 1.6.2 + * @type String + * @default + */ + uniScaleKey: 'shiftKey', + + /** + * When true, objects use center point as the origin of scale transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, objects use center point as the origin of rotate transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: false, + + /** + * Indicates which key enable centered Transform + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + centeredKey: 'altKey', + + /** + * Indicates which key enable alternate action on corner + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + altActionKey: 'shiftKey', + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @type Boolean + * @default + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @type Boolean + * @default + */ + selection: true, + + /** + * Indicates which key or keys enable multiple click selection + * Pass value as a string or array of strings + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or empty or containing any other string that is not a modifier key + * feature is disabled. + * @since 1.6.2 + * @type String|Array + * @default + */ + selectionKey: 'shiftKey', + + /** + * Indicates which key enable alternative selection + * in case of target overlapping with active object + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * For a series of reason that come from the general expectations on how + * things should work, this feature works only for preserveObjectStacking true. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * @since 1.6.5 + * @type null|String + * @default + */ + altSelectionKey: null, + + /** + * Color of selection + * @type String + * @default + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Default dash array pattern + * If not empty the selection border is dashed + * @type Array + */ + selectionDashArray: [], + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @type String + * @default + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @type Number + * @default + */ + selectionLineWidth: 1, + + /** + * Select only shapes that are fully contained in the dragged selection rectangle. + * @type Boolean + * @default + */ + selectionFullyContained: false, + + /** + * Default cursor value used when hovering over an object on canvas + * @type String + * @default + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @type String + * @default + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @type String + * @default + */ + defaultCursor: 'default', + + /** + * Cursor value used during free drawing + * @type String + * @default + */ + freeDrawingCursor: 'crosshair', + + /** + * Cursor value used for rotation point + * @type String + * @default + */ + rotationCursor: 'crosshair', + + /** + * Cursor value used for disabled elements ( corners with disabled action ) + * @type String + * @since 2.0.0 + * @default + */ + notAllowedCursor: 'not-allowed', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @type String + * @default + */ + containerClass: 'canvas-container', + + /** + * When true, object detection happens on per-pixel basis rather than on per-bounding-box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * Number of pixels around target pixel to tolerate (consider active) during object detection + * @type Number + * @default + */ + targetFindTolerance: 0, + + /** + * When true, target detection is skipped. Target detection will return always undefined. + * click selection won't work anymore, events will fire with no targets. + * if something is selected before setting it to true, it will be deselected at the first click. + * area selection will still work. check the `selection` property too. + * if you deactivate both, you should look into staticCanvas. + * @type Boolean + * @default + */ + skipTargetFind: false, + + /** + * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. + * After mousedown, mousemove creates a shape, + * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. + * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} + * @type Boolean + * @default + */ + isDrawingMode: false, + + /** + * Indicates whether objects should remain in current stack position when selected. + * When false objects are brought to top and rendered as part of the selection group + * @type Boolean + * @default + */ + preserveObjectStacking: false, + + /** + * Indicates the angle that an object will lock to while rotating. + * @type Number + * @since 1.6.7 + * @default + */ + snapAngle: 0, + + /** + * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. + * When `null`, the snapThreshold will default to the snapAngle. + * @type null|Number + * @since 1.6.7 + * @default + */ + snapThreshold: null, + + /** + * Indicates if the right click on canvas can output the context menu or not + * @type Boolean + * @since 1.6.5 + * @default + */ + stopContextMenu: false, + + /** + * Indicates if the canvas can fire right click events + * @type Boolean + * @since 1.6.5 + * @default + */ + fireRightClick: false, + + /** + * Indicates if the canvas can fire middle click events + * @type Boolean + * @since 1.7.8 + * @default + */ + fireMiddleClick: false, + + /** + * Keep track of the subTargets for Mouse Events + * @type fabric.Object[] + */ + targets: [], + + /** + * Keep track of the hovered target + * @type fabric.Object + * @private + */ + _hoveredTarget: null, + + /** + * hold the list of nested targets hovered + * @type fabric.Object[] + * @private + */ + _hoveredTargets: [], + + /** + * @private + */ + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEventListeners(); + + this._initRetinaScaling(); + + this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + + this.calcOffset(); + }, + + /** + * Divides objects in two groups, one to render immediately + * and one to render as activeGroup. + * @return {Array} objects to render immediately and pushes the other in the activeGroup. + */ + _chooseObjectsToRender: function() { + var activeObjects = this.getActiveObjects(), + object, objsToRender, activeGroupObjects; + + if (activeObjects.length > 0 && !this.preserveObjectStacking) { + objsToRender = []; + activeGroupObjects = []; + for (var i = 0, length = this._objects.length; i < length; i++) { + object = this._objects[i]; + if (activeObjects.indexOf(object) === -1 ) { + objsToRender.push(object); + } + else { + activeGroupObjects.push(object); + } + } + if (activeObjects.length > 1) { + this._activeObject._objects = activeGroupObjects; + } + objsToRender.push.apply(objsToRender, activeGroupObjects); + } + else { + objsToRender = this._objects; + } + return objsToRender; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { + this.clearContext(this.contextTop); + this.contextTopDirty = false; + } + if (this.hasLostContext) { + this.renderTopLayer(this.contextTop); + } + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + return this; + }, + + renderTopLayer: function(ctx) { + ctx.save(); + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._render(); + this.contextTopDirty = true; + } + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(ctx); + this.contextTopDirty = true; + } + ctx.restore(); + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + var ctx = this.contextTop; + this.clearContext(ctx); + this.renderTopLayer(ctx); + this.fire('after:render'); + return this; + }, + + /** + * @private + */ + _normalizePointer: function (object, pointer) { + var m = object.calcTransformMatrix(), + invertedM = fabric.util.invertTransform(m), + vptPointer = this.restorePointerVpt(pointer); + return fabric.util.transformPoint(vptPointer, invertedM); + }, + + /** + * Returns true if object is transparent at a certain location + * @param {fabric.Object} target Object to check + * @param {Number} x Left coordinate + * @param {Number} y Top coordinate + * @return {Boolean} + */ + isTargetTransparent: function (target, x, y) { + // in case the target is the activeObject, we cannot execute this optimization + // because we need to draw controls too. + if (target.shouldCache() && target._cacheCanvas && target !== this._activeObject) { + var normalizedPointer = this._normalizePointer(target, {x: x, y: y}), + targetRelativeX = Math.max(target.cacheTranslationX + (normalizedPointer.x * target.zoomX), 0), + targetRelativeY = Math.max(target.cacheTranslationY + (normalizedPointer.y * target.zoomY), 0); + + var isTransparent = fabric.util.isTransparent( + target._cacheContext, Math.round(targetRelativeX), Math.round(targetRelativeY), this.targetFindTolerance); + + return isTransparent; + } + + var ctx = this.contextCache, + originalColor = target.selectionBackgroundColor, v = this.viewportTransform; + + target.selectionBackgroundColor = ''; + + this.clearContext(ctx); + + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + target.render(ctx); + ctx.restore(); + + target.selectionBackgroundColor = originalColor; + + var isTransparent = fabric.util.isTransparent( + ctx, x, y, this.targetFindTolerance); + + return isTransparent; + }, + + /** + * takes an event and determines if selection key has been pressed + * @private + * @param {Event} e Event object + */ + _isSelectionKeyPressed: function(e) { + var selectionKeyPressed = false; + + if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { + selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); + } + else { + selectionKeyPressed = e[this.selectionKey]; + } + + return selectionKeyPressed; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldClearSelection: function (e, target) { + var activeObjects = this.getActiveObjects(), + activeObject = this._activeObject; + + return ( + !target + || + (target && + activeObject && + activeObjects.length > 1 && + activeObjects.indexOf(target) === -1 && + activeObject !== target && + !this._isSelectionKeyPressed(e)) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * centeredScaling from object can't override centeredScaling from canvas. + * this should be fixed, since object setting should take precedence over canvas. + * also this should be something that will be migrated in the control properties. + * as ability to define the origin of the transformation that the control provide. + * @private + * @param {fabric.Object} target + * @param {String} action + * @param {Boolean} altKey + */ + _shouldCenterTransform: function (target, action, altKey) { + if (!target) { + return; + } + + var centerTransform; + + if (action === 'scale' || action === 'scaleX' || action === 'scaleY' || action === 'resizing') { + centerTransform = this.centeredScaling || target.centeredScaling; + } + else if (action === 'rotate') { + centerTransform = this.centeredRotation || target.centeredRotation; + } + + return centerTransform ? !altKey : altKey; + }, + + /** + * should disappear before release 4.0 + * @private + */ + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; + + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } + + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + return origin; + }, + + /** + * @private + * @param {Boolean} alreadySelected true if target is already selected + * @param {String} corner a string representing the corner ml, mr, tl ... + * @param {Event} e Event object + * @param {fabric.Object} [target] inserted back to help overriding. Unused + */ + _getActionFromCorner: function(alreadySelected, corner, e, target) { + if (!corner || !alreadySelected) { + return 'drag'; + } + var control = target.controls[corner]; + return control.getActionName(e, control, target); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target, alreadySelected) { + if (!target) { + return; + } + + var pointer = this.getPointer(e), corner = target.__corner, + control = target.controls[corner], + actionHandler = (alreadySelected && corner) ? + control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, + action = this._getActionFromCorner(alreadySelected, corner, e, target), + origin = this._getOriginFromCorner(target, corner), + altKey = e[this.centeredKey], + transform = { + target: target, + action: action, + actionHandler: actionHandler, + corner: corner, + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + // used by transation + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + lastX: pointer.x, + lastY: pointer.y, + // unsure they are useful anymore. + // left: target.left, + // top: target.top, + theta: degreesToRadians(target.angle), + // end of unsure + width: target.width * target.scaleX, + shiftKey: e.shiftKey, + altKey: altKey, + original: fabric.util.saveObjectTransform(target), + }; + + if (this._shouldCenterTransform(target, action, altKey)) { + transform.originX = 'center'; + transform.originY = 'center'; + } + transform.original.originX = origin.x; + transform.original.originY = origin.y; + this._currentTransform = transform; + this._beforeTransform(e); + }, + + /** + * Set the cursor type of the canvas element + * @param {String} value Cursor type of the canvas element. + * @see http://www.w3.org/TR/css3-ui/#cursor + */ + setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx to draw the selection on + */ + _drawSelection: function (ctx) { + var selector = this._groupSelector, + viewportStart = new fabric.Point(selector.ex, selector.ey), + start = fabric.util.transformPoint(viewportStart, this.viewportTransform), + viewportExtent = new fabric.Point(selector.ex + selector.left, selector.ey + selector.top), + extent = fabric.util.transformPoint(viewportExtent, this.viewportTransform), + minX = Math.min(start.x, extent.x), + minY = Math.min(start.y, extent.y), + maxX = Math.max(start.x, extent.x), + maxY = Math.max(start.y, extent.y), + strokeOffset = this.selectionLineWidth / 2; + + if (this.selectionColor) { + ctx.fillStyle = this.selectionColor; + ctx.fillRect(minX, minY, maxX - minX, maxY - minY); + } + + if (!this.selectionLineWidth || !this.selectionBorderColor) { + return; + } + ctx.lineWidth = this.selectionLineWidth; + ctx.strokeStyle = this.selectionBorderColor; + + minX += strokeOffset; + minY += strokeOffset; + maxX -= strokeOffset; + maxY -= strokeOffset; + // selection border + fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); + ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); + }, + + /** + * Method that determines what object we are clicking on + * the skipGroup parameter is for internal use, is needed for shift+click action + * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target + * or the outside part of the corner. + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through + * @return {fabric.Object} the target found + */ + findTarget: function (e, skipGroup) { + if (this.skipTargetFind) { + return; + } + + var ignoreZoom = true, + pointer = this.getPointer(e, ignoreZoom), + activeObject = this._activeObject, + aObjects = this.getActiveObjects(), + activeTarget, activeTargetSubs, + isTouch = isTouchEvent(e), + shouldLookForActive = (aObjects.length > 1 && !skipGroup) || aObjects.length === 1; + + // first check current group (if one exists) + // active group does not check sub targets like normal groups. + // if active group just exits. + this.targets = []; + + // if we hit the corner of an activeObject, let's return that. + if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { + return activeObject; + } + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + return activeObject; + } + if (aObjects.length === 1 && + activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (!this.preserveObjectStacking) { + return activeObject; + } + else { + activeTarget = activeObject; + activeTargetSubs = this.targets; + this.targets = []; + } + } + var target = this._searchPossibleTargets(this._objects, pointer); + if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { + target = activeTarget; + this.targets = activeTargetSubs; + } + return target; + }, + + /** + * Checks point is inside the object. + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @param {fabric.Object} obj Object to test against + * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. + * @return {Boolean} true if point is contained within an area of given object + * @private + */ + _checkTarget: function(pointer, obj, globalPointer) { + if (obj && + obj.visible && + obj.evented && + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + obj.containsPoint(pointer) + ) { + if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { + var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); + if (!isTransparent) { + return true; + } + } + else { + return true; + } + } + }, + + /** + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * @param {Array} [objects] objects array to look into + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @return {fabric.Object} object that contains pointer + * @private + */ + _searchPossibleTargets: function(objects, pointer) { + // Cache all targets where their bounding box contains point. + var target, i = objects.length, subTarget; + // Do not check for currently grouped objects, since we check the parent group itself. + // until we call this function specifically to search inside the activeGroup + while (i--) { + var objToCheck = objects[i]; + var pointerToUse = objToCheck.group ? + this._normalizePointer(objToCheck.group, pointer) : pointer; + if (this._checkTarget(pointerToUse, objToCheck, pointer)) { + target = objects[i]; + if (target.subTargetCheck && target instanceof fabric.Group) { + subTarget = this._searchPossibleTargets(target._objects, pointer); + subTarget && this.targets.push(subTarget); + } + break; + } + } + return target; + }, + + /** + * Returns pointer coordinates without the effect of the viewport + * @param {Object} pointer with "x" and "y" number values + * @return {Object} object with "x" and "y" number values + */ + restorePointerVpt: function(pointer) { + return fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + }, + + /** + * Returns pointer coordinates relative to canvas. + * Can return coordinates with or without viewportTransform. + * ignoreZoom false gives back coordinates that represent + * the point clicked on canvas element. + * ignoreZoom true gives back coordinates after being processed + * by the viewportTransform ( sort of coordinates of what is displayed + * on the canvas where you are clicking. + * ignoreZoom true = HTMLElement coordinates relative to top,left + * ignoreZoom false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates + * compatible with the object.oCoords system. + * of the time. + * @param {Event} e + * @param {Boolean} ignoreZoom + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e, ignoreZoom) { + // return cached values if we are in the event processing chain + if (this._absolutePointer && !ignoreZoom) { + return this._absolutePointer; + } + if (this._pointer && ignoreZoom) { + return this._pointer; + } + + var pointer = getPointer(e), + upperCanvasEl = this.upperCanvasEl, + bounds = upperCanvasEl.getBoundingClientRect(), + boundsWidth = bounds.width || 0, + boundsHeight = bounds.height || 0, + cssScale; + + if (!boundsWidth || !boundsHeight ) { + if ('top' in bounds && 'bottom' in bounds) { + boundsHeight = Math.abs( bounds.top - bounds.bottom ); + } + if ('right' in bounds && 'left' in bounds) { + boundsWidth = Math.abs( bounds.right - bounds.left ); + } + } + + this.calcOffset(); + pointer.x = pointer.x - this._offset.left; + pointer.y = pointer.y - this._offset.top; + if (!ignoreZoom) { + pointer = this.restorePointerVpt(pointer); + } + + var retinaScaling = this.getRetinaScaling(); + if (retinaScaling !== 1) { + pointer.x /= retinaScaling; + pointer.y /= retinaScaling; + } + + if (boundsWidth === 0 || boundsHeight === 0) { + // If bounds are not available (i.e. not visible), do not apply scale. + cssScale = { width: 1, height: 1 }; + } + else { + cssScale = { + width: upperCanvasEl.width / boundsWidth, + height: upperCanvasEl.height / boundsHeight + }; + } + + return { + x: pointer.x * cssScale.width, + y: pointer.y * cssScale.height + }; + }, + + /** + * @private + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), + lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; + + // there is no need to create a new upperCanvas element if we have already one. + if (upperCanvasEl) { + upperCanvasEl.className = ''; + } + else { + upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl = upperCanvasEl; + } + fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); + + this.wrapperEl.appendChild(upperCanvasEl); + + this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); + this._applyCanvasStyle(upperCanvasEl); + this.contextTop = upperCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.width + 'px', + height: this.height + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @param {HTMLElement} element canvas element to apply styles on + */ + _applyCanvasStyle: function (element) { + var width = this.width || element.width, + height = this.height || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0, + 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', + '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Copy the entire inline style from one element (fromEl) to another (toEl) + * @private + * @param {Element} fromEl Element style is copied from + * @param {Element} toEl Element copied style is applied to + */ + _copyCanvasStyle: function (fromEl, toEl) { + toEl.style.cssText = fromEl.style.cssText; + }, + + /** + * Returns context of canvas where object selection is drawn + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Returns currently active object + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Returns an array with the current selected objects + * @return {fabric.Object} active object + */ + getActiveObjects: function () { + var active = this._activeObject; + if (active) { + if (active.type === 'activeSelection' && active._objects) { + return active._objects.slice(0); + } + else { + return [active]; + } + } + return []; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (obj === this._activeObject) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared', { target: obj }); + obj.fire('deselected'); + } + if (obj === this._hoveredTarget){ + this._hoveredTarget = null; + this._hoveredTargets = []; + } + this.callSuper('_onObjectRemoved', obj); + }, + + /** + * @private + * Compares the old activeObject with the current one and fires correct events + * @param {fabric.Object} obj old activeObject + */ + _fireSelectionEvents: function(oldObjects, e) { + var somethingChanged = false, objects = this.getActiveObjects(), + added = [], removed = []; + oldObjects.forEach(function(oldObject) { + if (objects.indexOf(oldObject) === -1) { + somethingChanged = true; + oldObject.fire('deselected', { + e: e, + target: oldObject + }); + removed.push(oldObject); + } + }); + objects.forEach(function(object) { + if (oldObjects.indexOf(object) === -1) { + somethingChanged = true; + object.fire('selected', { + e: e, + target: object + }); + added.push(object); + } + }); + if (oldObjects.length > 0 && objects.length > 0) { + somethingChanged && this.fire('selection:updated', { + e: e, + selected: added, + deselected: removed, + // added for backward compatibility + // deprecated + updated: added[0] || removed[0], + target: this._activeObject, + }); + } + else if (objects.length > 0) { + this.fire('selection:created', { + e: e, + selected: added, + target: this._activeObject, + }); + } + else if (oldObjects.length > 0) { + this.fire('selection:cleared', { + e: e, + deselected: removed, + }); + } + }, + + /** + * Sets given object as the only active object on canvas + * @param {fabric.Object} object Object to set as an active one + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + var currentActives = this.getActiveObjects(); + this._setActiveObject(object, e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * This is a private method for now. + * This is supposed to be equivalent to setActiveObject but without firing + * any event. There is commitment to have this stay this way. + * This is the functional part of setActiveObject. + * @private + * @param {Object} object to set as active + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {Boolean} true if the selection happened + */ + _setActiveObject: function(object, e) { + if (this._activeObject === object) { + return false; + } + if (!this._discardActiveObject(e, object)) { + return false; + } + if (object.onSelect({ e: e })) { + return false; + } + this._activeObject = object; + return true; + }, + + /** + * This is a private method for now. + * This is supposed to be equivalent to discardActiveObject but without firing + * any events. There is commitment to have this stay this way. + * This is the functional part of discardActiveObject. + * @param {Event} [e] Event (passed along when firing "object:deselected") + * @param {Object} object to set as active + * @return {Boolean} true if the selection happened + * @private + */ + _discardActiveObject: function(e, object) { + var obj = this._activeObject; + if (obj) { + // onDeselect return TRUE to cancel selection; + if (obj.onDeselect({ e: e, object: object })) { + return false; + } + this._activeObject = null; + } + return true; + }, + + /** + * Discards currently active object and fire events. If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parameter and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @param {event} e + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); + if (currentActives.length) { + this.fire('before:selection:cleared', { target: activeObject, e: e }); + } + this._discardActiveObject(e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * Clears a canvas element and removes all event listeners + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + var wrapper = this.wrapperEl; + this.removeListeners(); + wrapper.removeChild(this.upperCanvasEl); + wrapper.removeChild(this.lowerCanvasEl); + this.contextCache = null; + this.contextTop = null; + ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + if (wrapper.parentNode) { + wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); + } + delete this.wrapperEl; + fabric.StaticCanvas.prototype.dispose.call(this); + return this; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + // this.discardActiveGroup(); + this.discardActiveObject(); + this.clearContext(this.contextTop); + return this.callSuper('clear'); + }, + + /** + * Draws objects' controls (borders/controls) + * @param {CanvasRenderingContext2D} ctx Context to render controls on + */ + drawControls: function(ctx) { + var activeObject = this._activeObject; + + if (activeObject) { + activeObject._renderControls(ctx); + } + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + //If the object is part of the current selection group, it should + //be transformed appropriately + //i.e. it should be serialised as it would appear if the selection group + //were to be destroyed. + var originalProperties = this._realizeGroupTransformOnObject(instance), + object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); + //Undo the damage we did by changing all of its properties + this._unwindGroupTransformOnObject(instance, originalProperties); + return object; + }, + + /** + * Realises an object's group transformation on it + * @private + * @param {fabric.Object} [instance] the object to transform (gets mutated) + * @returns the original values of instance which were changed + */ + _realizeGroupTransformOnObject: function(instance) { + if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { + var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; + //Copy all the positionally relevant properties across now + var originalValues = {}; + layoutProps.forEach(function(prop) { + originalValues[prop] = instance[prop]; + }); + fabric.util.addTransformToObject(instance, this._activeObject.calcOwnMatrix()); + return originalValues; + } + else { + return null; + } + }, + + /** + * Restores the changed properties of instance + * @private + * @param {fabric.Object} [instance] the object to un-transform (gets mutated) + * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject + */ + _unwindGroupTransformOnObject: function(instance, originalValues) { + if (originalValues) { + instance.set(originalValues); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + //If the object is in a selection group, simulate what would happen to that + //object when the group is deselected + var originalProperties = this._realizeGroupTransformOnObject(instance); + this.callSuper('_setSVGObject', markup, instance, reviver); + this._unwindGroupTransformOnObject(instance, originalProperties); + }, + + setViewportTransform: function (vpt) { + if (this.renderOnAddRemove && this._activeObject && this._activeObject.isEditing) { + this._activeObject.clearContextTop(); + } + fabric.StaticCanvas.prototype.setViewportTransform.call(this, vpt); + } + }); + + // copying static properties manually to work around Opera's bug, + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } +})(); + + +(function() { + + var addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + RIGHT_CLICK = 3, MIDDLE_CLICK = 2, LEFT_CLICK = 1, + addEventOptions = { passive: false }; + + function checkClick(e, value) { + return e.button && (e.button === value - 1); + } + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Contains the id of the touch event that owns the fabric transform + * @type Number + * @private + */ + mainTouchId: null, + + /** + * Adds mouse listeners to canvas + * @private + */ + _initEventListeners: function () { + // in case we initialized the class twice. This should not happen normally + // but in some kind of applications where the canvas element may be changed + // this is a workaround to having double listeners. + this.removeListeners(); + this._bindEvents(); + this.addOrRemove(addListener, 'add'); + }, + + /** + * return an event prefix pointer or mouse. + * @private + */ + _getEventPrefix: function () { + return this.enablePointerEvents ? 'pointer' : 'mouse'; + }, + + addOrRemove: function(functor, eventjsFunctor) { + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + functor(fabric.window, 'resize', this._onResize); + functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + functor(canvasElement, eventTypePrefix + 'out', this._onMouseOut); + functor(canvasElement, eventTypePrefix + 'enter', this._onMouseEnter); + functor(canvasElement, 'wheel', this._onMouseWheel); + functor(canvasElement, 'contextmenu', this._onContextMenu); + functor(canvasElement, 'dblclick', this._onDoubleClick); + functor(canvasElement, 'dragover', this._onDragOver); + functor(canvasElement, 'dragenter', this._onDragEnter); + functor(canvasElement, 'dragleave', this._onDragLeave); + functor(canvasElement, 'drop', this._onDrop); + if (!this.enablePointerEvents) { + functor(canvasElement, 'touchstart', this._onTouchStart, addEventOptions); + } + if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { + eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); + eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); + eventjs[eventjsFunctor](canvasElement, 'orientation', this._onOrientationChange); + eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); + eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress); + } + }, + + /** + * Removes all event listeners + */ + removeListeners: function() { + this.addOrRemove(removeListener, 'remove'); + // if you dispose on a mouseDown, before mouse up, you need to clean document to... + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + }, + + /** + * @private + */ + _bindEvents: function() { + if (this.eventsBound) { + // for any reason we pass here twice we do not want to bind events twice. + return; + } + this._onMouseDown = this._onMouseDown.bind(this); + this._onTouchStart = this._onTouchStart.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onLongPress = this._onLongPress.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseEnter = this._onMouseEnter.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onDoubleClick = this._onDoubleClick.bind(this); + this._onDragOver = this._onDragOver.bind(this); + this._onDragEnter = this._simpleEventHandler.bind(this, 'dragenter'); + this._onDragLeave = this._simpleEventHandler.bind(this, 'dragleave'); + this._onDrop = this._simpleEventHandler.bind(this, 'drop'); + this.eventsBound = true; + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, self) { + this.__onTransformGesture && this.__onTransformGesture(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, self) { + this.__onDrag && this.__onDrag(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on wheel event + */ + _onMouseWheel: function(e) { + this.__onMouseWheel(e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseOut: function(e) { + var target = this._hoveredTarget; + this.fire('mouse:out', { target: target, e: e }); + this._hoveredTarget = null; + target && target.fire('mouseout', { e: e }); + + var _this = this; + this._hoveredTargets.forEach(function(_target){ + _this.fire('mouse:out', { target: target, e: e }); + _target && target.fire('mouseout', { e: e }); + }); + this._hoveredTargets = []; + + if (this._iTextInstances) { + this._iTextInstances.forEach(function(obj) { + if (obj.isEditing) { + obj.hiddenTextarea.focus(); + } + }); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mouseenter + */ + _onMouseEnter: function(e) { + // This find target and consequent 'mouse:over' is used to + // clear old instances on hovered target. + // calling findTarget has the side effect of killing target.__corner. + // as a short term fix we are not firing this if we are currently transforming. + // as a long term fix we need to separate the action of finding a target with the + // side effects we added to it. + if (!this._currentTransform && !this.findTarget(e)) { + this.fire('mouse:over', { target: null, e: e }); + this._hoveredTarget = null; + this._hoveredTargets = []; + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e, self) { + this.__onOrientationChange && this.__onOrientationChange(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e, self) { + this.__onShake && this.__onShake(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onLongPress: function(e, self) { + this.__onLongPress && this.__onLongPress(e, self); + }, + + /** + * prevent default to allow drop event to be fired + * @private + * @param {Event} [e] Event object fired on Event.js shake + */ + _onDragOver: function(e) { + e.preventDefault(); + var target = this._simpleEventHandler('dragover', e); + this._fireEnterLeaveEvents(target, e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onContextMenu: function (e) { + if (this.stopContextMenu) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onDoubleClick: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'dblclick'); + this._resetTransformEventData(e); + }, + + /** + * Return a the id of an event. + * returns either the pointerId or the identifier or 0 for the mouse event + * @private + * @param {Event} evt Event object + */ + getPointerId: function(evt) { + var changedTouches = evt.changedTouches; + + if (changedTouches) { + return changedTouches[0] && changedTouches[0].identifier; + } + + if (this.enablePointerEvents) { + return evt.pointerId; + } + + return -1; + }, + + /** + * Determines if an event has the id of the event that is considered main + * @private + * @param {evt} event Event object + */ + _isMainEvent: function(evt) { + if (evt.isPrimary === true) { + return true; + } + if (evt.isPrimary === false) { + return false; + } + if (evt.type === 'touchend' && evt.touches.length === 0) { + return true; + } + if (evt.changedTouches) { + return evt.changedTouches[0].identifier === this.mainTouchId; + } + return true; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchStart: function(e) { + e.preventDefault(); + if (this.mainTouchId === null) { + this.mainTouchId = this.getPointerId(e); + } + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + addListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + addListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + // Unbind mousedown to prevent double triggers from touch devices + removeListener(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown: function (e) { + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchEnd: function(e) { + if (e.touches.length > 0) { + // if there are still touches stop here + return; + } + this.__onMouseUp(e); + this._resetTransformEventData(); + this.mainTouchId = null; + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + var _this = this; + if (this._willAddMouseDown) { + clearTimeout(this._willAddMouseDown); + } + this._willAddMouseDown = setTimeout(function() { + // Wait 400ms before rebinding mousedown to prevent double triggers + // from touch devices + addListener(_this.upperCanvasEl, eventTypePrefix + 'down', _this._onMouseDown); + _this._willAddMouseDown = 0; + }, 400); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUp: function (e) { + this.__onMouseUp(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + if (this._isMainEvent(e)) { + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMove: function (e) { + !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); + this.__onMouseMove(e); + }, + + /** + * @private + */ + _onResize: function () { + this.calcOffset(); + }, + + /** + * Decides whether the canvas should be redrawn in mouseup and mousedown events. + * @private + * @param {Object} target + */ + _shouldRender: function(target) { + var activeObject = this._activeObject; + + if ( + !!activeObject !== !!target || + (activeObject && target && (activeObject !== target)) + ) { + // this covers: switch of target, from target to no target, selection of target + // multiSelection with key and mouse + return true; + } + else if (activeObject && activeObject.isEditing) { + // if we mouse up/down over a editing textbox a cursor change, + // there is no need to re render + return false; + } + return false; + }, + + /** + * Method that defines the actions when mouse is released on canvas. + * The method resets the currentTransform parameters, store the image corner + * position in the image object and render the canvas on top. + * @private + * @param {Event} e Event object fired on mouseup + */ + __onMouseUp: function (e) { + var target, transform = this._currentTransform, + groupSelector = this._groupSelector, shouldRender = false, + isClick = (!groupSelector || (groupSelector.left === 0 && groupSelector.top === 0)); + this._cacheTransformEventData(e); + target = this._target; + this._handleEvent(e, 'up:before'); + // if right/middle click just fire events and return + // target undefined will make the _handleEvent search the target + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'up', RIGHT_CLICK, isClick); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); + } + this._resetTransformEventData(); + return; + } + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._onMouseUpInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + if (transform) { + this._finalizeCurrentTransform(e); + shouldRender = transform.actionPerformed; + } + if (!isClick) { + var targetWasActive = target === this._activeObject; + this._maybeGroupObjects(e); + if (!shouldRender) { + shouldRender = ( + this._shouldRender(target) || + (!targetWasActive && target === this._activeObject) + ); + } + } + if (target) { + if (target.selectable && target !== this._activeObject && target.activeOn === 'up') { + this.setActiveObject(target, e); + shouldRender = true; + } + else { + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + var control = target.controls[corner], + mouseUpHandler = control && control.getMouseUpHandler(e, target, control); + if (mouseUpHandler) { + var pointer = this.getPointer(e); + mouseUpHandler(e, transform, pointer.x, pointer.y); + } + } + target.isMoving = false; + } + this._setCursorFromEvent(e, target); + this._handleEvent(e, 'up', LEFT_CLICK, isClick); + this._groupSelector = null; + this._currentTransform = null; + // reset the target information about which corner is selected + target && (target.__corner = 0); + if (shouldRender) { + this.requestRenderAll(); + } + else if (!isClick) { + this.renderTop(); + } + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @return {Fabric.Object} target return the the target found, for internal reasons. + */ + _simpleEventHandler: function(eventType, e) { + var target = this.findTarget(e), + targets = this.targets, + options = { + e: e, + target: target, + subTargets: targets, + }; + this.fire(eventType, options); + target && target.fire(eventType, options); + if (!targets) { + return target; + } + for (var i = 0; i < targets.length; i++) { + targets[i].fire(eventType, options); + } + return target; + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @param {fabric.Object} targetObj receiving event + * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right + * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. + */ + _handleEvent: function(e, eventType, button, isClick) { + var target = this._target, + targets = this.targets || [], + options = { + e: e, + target: target, + subTargets: targets, + button: button || LEFT_CLICK, + isClick: isClick || false, + pointer: this._pointer, + absolutePointer: this._absolutePointer, + transform: this._currentTransform + }; + if (eventType === 'up') { + options.currentTarget = this.findTarget(e); + options.currentSubTargets = this.targets; + } + this.fire('mouse:' + eventType, options); + target && target.fire('mouse' + eventType, options); + for (var i = 0; i < targets.length; i++) { + targets[i].fire('mouse' + eventType, options); + } + }, + + /** + * @private + * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event + */ + _finalizeCurrentTransform: function(e) { + + var transform = this._currentTransform, + target = transform.target, + eventName, + options = { + e: e, + target: target, + transform: transform, + action: transform.action, + }; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { + if (transform.actionPerformed) { + // this is not friendly to the new control api. + // is deprecated. + eventName = this._addEventOptions(options, transform); + this._fire(eventName, options); + } + this._fire('modified', options); + } + }, + + /** + * Mutate option object in order to add by property and give back the event name. + * @private + * @deprecated since 4.2.0 + * @param {Object} options to mutate + * @param {Object} transform to inspect action from + */ + _addEventOptions: function(options, transform) { + // we can probably add more details at low cost + // scale change, rotation changes, translation changes + var eventName, by; + switch (transform.action) { + case 'scaleX': + eventName = 'scaled'; + by = 'x'; + break; + case 'scaleY': + eventName = 'scaled'; + by = 'y'; + break; + case 'skewX': + eventName = 'skewed'; + by = 'x'; + break; + case 'skewY': + eventName = 'skewed'; + by = 'y'; + break; + case 'scale': + eventName = 'scaled'; + by = 'equally'; + break; + case 'rotate': + eventName = 'rotated'; + break; + case 'drag': + eventName = 'moved'; + break; + } + options.by = by; + return eventName; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDownInDrawingMode: function(e) { + this._isCurrentlyDrawing = true; + if (this.getActiveObject()) { + this.discardActiveObject(e).requestRenderAll(); + } + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); + this._handleEvent(e, 'down'); + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseMove(pointer, { e: e, pointer: pointer }); + } + this.setCursor(this.freeDrawingCursor); + this._handleEvent(e, 'move'); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + var pointer = this.getPointer(e); + this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); + this._handleEvent(e, 'up'); + }, + + /** + * Method that defines the actions when mouse is clicked on canvas. + * The method inits the currentTransform parameters and renders all the + * canvas so the current image can be placed on the top canvas and the rest + * in on the container one. + * @private + * @param {Event} e Event object fired on mousedown + */ + __onMouseDown: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'down:before'); + var target = this._target; + // if right click just fire events + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'down', RIGHT_CLICK); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'down', MIDDLE_CLICK); + } + return; + } + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) { + return; + } + + var pointer = this._pointer; + // save pointer for check in __onMouseUp event + this._previousPointer = pointer; + var shouldRender = this._shouldRender(target), + shouldGroup = this._shouldGroup(e, target); + if (this._shouldClearSelection(e, target)) { + this.discardActiveObject(e); + } + else if (shouldGroup) { + this._handleGrouping(e, target); + target = this._activeObject; + } + + if (this.selection && (!target || + (!target.selectable && !target.isEditing && target !== this._activeObject))) { + this._groupSelector = { + ex: this._absolutePointer.x, + ey: this._absolutePointer.y, + top: 0, + left: 0 + }; + } + + if (target) { + var alreadySelected = target === this._activeObject; + if (target.selectable && target.activeOn === 'down') { + this.setActiveObject(target, e); + } + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + target.__corner = corner; + if (target === this._activeObject && (corner || !shouldGroup)) { + this._setupCurrentTransform(e, target, alreadySelected); + var control = target.controls[corner], + pointer = this.getPointer(e), + mouseDownHandler = control && control.getMouseDownHandler(e, target, control); + if (mouseDownHandler) { + mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y); + } + } + } + this._handleEvent(e, 'down'); + // we must renderAll so that we update the visuals + (shouldRender || shouldGroup) && this.requestRenderAll(); + }, + + /** + * reset cache form common information needed during event processing + * @private + */ + _resetTransformEventData: function() { + this._target = null; + this._pointer = null; + this._absolutePointer = null; + }, + + /** + * Cache common information needed during event processing + * @private + * @param {Event} e Event object fired on event + */ + _cacheTransformEventData: function(e) { + // reset in order to avoid stale caching + this._resetTransformEventData(); + this._pointer = this.getPointer(e, true); + this._absolutePointer = this.restorePointerVpt(this._pointer); + this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null; + }, + + /** + * @private + */ + _beforeTransform: function(e) { + var t = this._currentTransform; + this.stateful && t.target.saveState(); + this.fire('before:transform', { + e: e, + transform: t, + }); + }, + + /** + * Method that defines the actions when mouse is hovering the canvas. + * The currentTransform parameter will define whether the user is rotating/scaling/translating + * an image or neither of them (only hovering). A group selection is also possible and would cancel + * all any other type of action. + * In case of an image transformation only the top canvas will be rendered. + * @private + * @param {Event} e Event object fired on mousemove + */ + __onMouseMove: function (e) { + this._handleEvent(e, 'move:before'); + this._cacheTransformEventData(e); + var target, pointer; + + if (this.isDrawingMode) { + this._onMouseMoveInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + pointer = this._absolutePointer; + + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; + + this.renderTop(); + } + else if (!this._currentTransform) { + target = this.findTarget(e) || null; + this._setCursorFromEvent(e, target); + this._fireOverOutEvents(target, e); + } + else { + this._transformObject(e); + } + this._handleEvent(e, 'move'); + this._resetTransformEventData(); + }, + + /** + * Manage the mouseout, mouseover events for the fabric object on the canvas + * @param {Fabric.Object} target the target where the target from the mousemove event + * @param {Event} e Event object fired on mousemove + * @private + */ + _fireOverOutEvents: function(target, e) { + var _hoveredTarget = this._hoveredTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _hoveredTarget, + evtOut: 'mouseout', + canvasEvtOut: 'mouse:out', + evtIn: 'mouseover', + canvasEvtIn: 'mouse:over', + }); + for (var i = 0; i < length; i++){ + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'mouseout', + evtIn: 'mouseover', + }); + } + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + }, + + /** + * Manage the dragEnter, dragLeave events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the onDrag event + * @param {Event} e Event object fired on ondrag + * @private + */ + _fireEnterLeaveEvents: function(target, e) { + var _draggedoverTarget = this._draggedoverTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _draggedoverTarget, + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + for (var i = 0; i < length; i++) { + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + } + this._draggedoverTarget = target; + }, + + /** + * Manage the synthetic in/out events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the supported events + * @param {Event} e Event object fired + * @param {Object} config configuration for the function to work + * @param {String} config.targetName property on the canvas where the old target is stored + * @param {String} [config.canvasEvtOut] name of the event to fire at canvas level for out + * @param {String} config.evtOut name of the event to fire for out + * @param {String} [config.canvasEvtIn] name of the event to fire at canvas level for in + * @param {String} config.evtIn name of the event to fire for in + * @private + */ + fireSyntheticInOutEvents: function(target, e, config) { + var inOpt, outOpt, oldTarget = config.oldTarget, outFires, inFires, + targetChanged = oldTarget !== target, canvasEvtIn = config.canvasEvtIn, canvasEvtOut = config.canvasEvtOut; + if (targetChanged) { + inOpt = { e: e, target: target, previousTarget: oldTarget }; + outOpt = { e: e, target: oldTarget, nextTarget: target }; + } + inFires = target && targetChanged; + outFires = oldTarget && targetChanged; + if (outFires) { + canvasEvtOut && this.fire(canvasEvtOut, outOpt); + oldTarget.fire(config.evtOut, outOpt); + } + if (inFires) { + canvasEvtIn && this.fire(canvasEvtIn, inOpt); + target.fire(config.evtIn, inOpt); + } + }, + + /** + * Method that defines actions when an Event Mouse Wheel + * @param {Event} e Event object fired on mouseup + */ + __onMouseWheel: function(e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'wheel'); + this._resetTransformEventData(); + }, + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + var pointer = this.getPointer(e), + transform = this._currentTransform; + + transform.reset = false; + transform.shiftKey = e.shiftKey; + transform.altKey = e[this.centeredKey]; + + this._performTransformAction(e, transform, pointer); + transform.actionPerformed && this.requestRenderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + action = transform.action, + actionPerformed = false, + actionHandler = transform.actionHandler; + // this object could be created from the function in the control handlers + + + if (actionHandler) { + actionPerformed = actionHandler(e, transform, x, y); + } + if (action === 'drag' && actionPerformed) { + transform.target.isMoving = true; + this.setCursor(transform.target.moveCursor || this.moveCursor); + } + transform.actionPerformed = transform.actionPerformed || actionPerformed; + }, + + /** + * @private + */ + _fire: fabric.controlsUtils.fireEvent, + + /** + * Sets the cursor depending on where the canvas is being hovered. + * Note: very buggy in Opera + * @param {Event} e Event object + * @param {Object} target Object that the mouse is hovering, if so. + */ + _setCursorFromEvent: function (e, target) { + if (!target) { + this.setCursor(this.defaultCursor); + return false; + } + var hoverCursor = target.hoverCursor || this.hoverCursor, + activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? + this._activeObject : null, + // only show proper corner when group selection is not active + corner = (!activeSelection || !activeSelection.contains(target)) + // here we call findTargetCorner always with undefined for the touch parameter. + // we assume that if you are using a cursor you do not need to interact with + // the bigger touch area. + && target._findTargetCorner(this.getPointer(e, true)); + + if (!corner) { + if (target.subTargetCheck){ + // hoverCursor should come from top-most subTarget, + // so we walk the array backwards + this.targets.concat().reverse().map(function(_target){ + hoverCursor = _target.hoverCursor || hoverCursor; + }); + } + this.setCursor(hoverCursor); + } + else { + this.setCursor(this.getCornerCursor(corner, target, e)); + } + }, + + /** + * @private + */ + getCornerCursor: function(corner, target, e) { + var control = target.controls[corner]; + return control.cursorStyleHandler(e, control, target); + } + }); +})(); + + +(function() { + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this._activeObject; + return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + var activeObject = this._activeObject; + // avoid multi select when shift click on a corner + if (activeObject.__corner) { + return; + } + if (target === activeObject) { + // if it's a group, find target again, using activeGroup objects + target = this.findTarget(e, true); + // if even object is not found or we are on activeObjectCorner, bail out + if (!target || !target.selectable) { + return; + } + } + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(target, e); + } + else { + this._createActiveSelection(target, e); + } + }, + + /** + * @private + */ + _updateActiveSelection: function(target, e) { + var activeSelection = this._activeObject, + currentActiveObjects = activeSelection._objects.slice(0); + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + if (activeSelection.size() === 1) { + // activate last remaining object + this._setActiveObject(activeSelection.item(0), e); + } + } + else { + activeSelection.addWithUpdate(target); + this._hoveredTarget = activeSelection; + this._hoveredTargets = this.targets.concat(); + } + this._fireSelectionEvents(currentActiveObjects, e); + }, + + /** + * @private + */ + _createActiveSelection: function(target, e) { + var currentActives = this.getActiveObjects(), group = this._createGroup(target); + this._hoveredTarget = group; + // ISSUE 4115: should we consider subTargets here? + // this._hoveredTargets = []; + // this._hoveredTargets = this.targets.concat(); + this._setActiveObject(group, e); + this._fireSelectionEvents(currentActives, e); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + var objects = this._objects, + isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), + groupObjects = isActiveLower + ? [this._activeObject, target] + : [target, this._activeObject]; + this._activeObject.isEditing && this._activeObject.exitEditing(); + return new fabric.ActiveSelection(groupObjects, { + canvas: this + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(e), + aGroup; + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + aGroup = new fabric.ActiveSelection(group.reverse(), { + canvas: this + }); + this.setActiveObject(aGroup, e); + } + }, + + /** + * @private + */ + _collectObjects: function(e) { + var group = [], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + allowIntersect = !this.selectionFullyContained, + isClick = x1 === x2 && y1 === y2; + // we iterate reverse order to collect top first in case of click. + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) { + continue; + } + + if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2, true)) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2, true) || + (allowIntersect && currentObject.containsPoint(selectionX1Y1, null, true)) || + (allowIntersect && currentObject.containsPoint(selectionX2Y2, null, true)) + ) { + group.push(currentObject); + // only add one object if it's a click + if (isClick) { + break; + } + } + } + + if (group.length > 1) { + group = group.filter(function(object) { + return !object.onSelect({ e: e }); + }); + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + this.setCursor(this.defaultCursor); + // clear selection and current transformation + this._groupSelector = null; + } + }); + +})(); + + +(function () { + fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately + * @param {Object} [options] Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @example Generate jpeg dataURL with lower quality + * var dataURL = canvas.toDataURL({ + * format: 'jpeg', + * quality: 0.8 + * }); + * @example Generate cropped png dataURL (clipping of canvas) + * var dataURL = canvas.toDataURL({ + * format: 'png', + * left: 100, + * top: 100, + * width: 200, + * height: 200 + * }); + * @example Generate double scaled png dataURL + * var dataURL = canvas.toDataURL({ + * format: 'png', + * multiplier: 2 + * }); + */ + toDataURL: function (options) { + options || (options = { }); + + var format = options.format || 'png', + quality = options.quality || 1, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), + canvasEl = this.toCanvasElement(multiplier, options); + return fabric.util.toDataURL(canvasEl, format, quality); + }, + + /** + * Create a new HTMLCanvas element painted with the current canvas content. + * No need to resize the actual one or repaint it. + * Will transfer object ownership to a new canvas, paint it, and set everything back. + * This is an intermediary step used to get to a dataUrl but also it is useful to + * create quick image copies of a canvas without passing for the dataUrl string + * @param {Number} [multiplier] a zoom factor. + * @param {Object} [cropping] Cropping informations + * @param {Number} [cropping.left] Cropping left offset. + * @param {Number} [cropping.top] Cropping top offset. + * @param {Number} [cropping.width] Cropping width. + * @param {Number} [cropping.height] Cropping height. + */ + toCanvasElement: function(multiplier, cropping) { + multiplier = multiplier || 1; + cropping = cropping || { }; + var scaledWidth = (cropping.width || this.width) * multiplier, + scaledHeight = (cropping.height || this.height) * multiplier, + zoom = this.getZoom(), + originalWidth = this.width, + originalHeight = this.height, + newZoom = zoom * multiplier, + vp = this.viewportTransform, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, + originalInteractive = this.interactive, + newVp = [newZoom, 0, 0, newZoom, translateX, translateY], + originalRetina = this.enableRetinaScaling, + canvasEl = fabric.util.createCanvasElement(), + originalContextTop = this.contextTop; + canvasEl.width = scaledWidth; + canvasEl.height = scaledHeight; + this.contextTop = null; + this.enableRetinaScaling = false; + this.interactive = false; + this.viewportTransform = newVp; + this.width = scaledWidth; + this.height = scaledHeight; + this.calcViewportBoundaries(); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); + this.viewportTransform = vp; + this.width = originalWidth; + this.height = originalHeight; + this.calcViewportBoundaries(); + this.interactive = originalInteractive; + this.enableRetinaScaling = originalRetina; + this.contextTop = originalContextTop; + return canvasEl; + }, + }); + +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + /** + * Populates canvas with data from the specified JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} + * @example loadFromJSON + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * @example loadFromJSON with reviver + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * // `o` = json object + * // `object` = fabric.Object instance + * // ... do some stuff ... + * }); + */ + loadFromJSON: function (json, callback, reviver) { + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : fabric.util.object.clone(json); + + var _this = this, + clipPath = serialized.clipPath, + renderOnAddRemove = this.renderOnAddRemove; + + this.renderOnAddRemove = false; + + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); + _this._setBgOverlay(serialized, function () { + if (clipPath) { + _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { + _this.clipPath = enlivenedCanvasClip[0]; + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + }); + } + else { + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + } + }); + }, reviver); + return this; + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Array} restored canvas objects + * @param {Function} cached renderOnAddRemove callback + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { + var _this = this; + enlivenedObjects.forEach(function(obj, index) { + // we splice the array just in case some custom classes restored from JSON + // will add more object to canvas at canvas init. + _this.insertAt(obj, index); + }); + this.renderOnAddRemove = renderOnAddRemove; + // remove parts i cannot set as options + delete serialized.objects; + delete serialized.backgroundImage; + delete serialized.overlayImage; + delete serialized.background; + delete serialized.overlay; + // this._initOptions does too many things to just + // call it. Normally loading an Object from JSON + // create the Object instance. Here the Canvas is + // already an instance and we are just loading things over it + this._setOptions(serialized); + this.renderAll(); + callback && callback(); + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + }, + + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + if (!objects || objects.length === 0) { + callback && callback([]); + return; + } + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + */ + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.util.createCanvasElement(); + + el.width = this.width; + el.height = this.height; + + var clone = new fabric.Canvas(el); + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + degreesToRadians = fabric.util.degreesToRadians, + objectCaching = !fabric.isLikelyNode, + ALIASING_LIMIT = 2; + + if (fabric.Object) { + return; + } + + /** + * Root object class from which all 2d shape classes inherit from + * @class fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects} + * @see {@link fabric.Object#initialize} for constructor definition + * + * @fires added + * @fires removed + * + * @fires selected + * @fires deselected + * @fires modified + * @fires modified + * @fires moved + * @fires scaled + * @fires rotated + * @fires skewed + * + * @fires rotating + * @fires scaling + * @fires moving + * @fires skewing + * + * @fires mousedown + * @fires mouseup + * @fires mouseover + * @fires mouseout + * @fires mousewheel + * @fires mousedblclick + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + */ + fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc.). + * Note that this property is meant to be read-only and not meant to be modified. + * If you modify, certain parts of Fabric (such as JSON loading) won't work correctly. + * @type String + * @default + */ + type: 'object', + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originX: 'left', + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originY: 'top', + + /** + * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} + * @type Number + * @default + */ + top: 0, + + /** + * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} + * @type Number + * @default + */ + left: 0, + + /** + * Object width + * @type Number + * @default + */ + width: 0, + + /** + * Object height + * @type Number + * @default + */ + height: 0, + + /** + * Object scale factor (horizontal) + * @type Number + * @default + */ + scaleX: 1, + + /** + * Object scale factor (vertical) + * @type Number + * @default + */ + scaleY: 1, + + /** + * When true, an object is rendered as flipped horizontally + * @type Boolean + * @default + */ + flipX: false, + + /** + * When true, an object is rendered as flipped vertically + * @type Boolean + * @default + */ + flipY: false, + + /** + * Opacity of an object + * @type Number + * @default + */ + opacity: 1, + + /** + * Angle of rotation of an object (in degrees) + * @type Number + * @default + */ + angle: 0, + + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default + */ + skewX: 0, + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default + */ + skewY: 0, + + /** + * Size of object's controlling corners (in pixels) + * @type Number + * @default + */ + cornerSize: 13, + + /** + * Size of object's controlling corners when touch interaction is detected + * @type Number + * @default + */ + touchCornerSize: 24, + + /** + * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) + * @type Boolean + * @default + */ + transparentCorners: true, + + /** + * Default cursor value used when hovering over this object on canvas + * @type String + * @default + */ + hoverCursor: null, + + /** + * Default cursor value used when moving this object on canvas + * @type String + * @default + */ + moveCursor: null, + + /** + * Padding between object and its controlling borders (in pixels) + * @type Number + * @default + */ + padding: 0, + + /** + * Color of controlling borders of an object (when it's active) + * @type String + * @default + */ + borderColor: 'rgb(178,204,255)', + + /** + * Array specifying dash pattern of an object's borders (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + borderDashArray: null, + + /** + * Color of controlling corners of an object (when it's active) + * @type String + * @default + */ + cornerColor: 'rgb(178,204,255)', + + /** + * Color of controlling corners of an object (when it's active and transparentCorners false) + * @since 1.6.2 + * @type String + * @default + */ + cornerStrokeColor: null, + + /** + * Specify style of control, 'rect' or 'circle' + * @since 1.6.2 + * @type String + */ + cornerStyle: 'rect', + + /** + * Array specifying dash pattern of an object's control (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + cornerDashArray: null, + + /** + * When true, this object will use center point as the origin of transformation + * when being scaled via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, this object will use center point as the origin of transformation + * when being rotated via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: true, + + /** + * Color of object's fill + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + fill: 'rgb(0,0,0)', + + /** + * Fill rule used to fill an object + * accepted values are nonzero, evenodd + * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) + * @type String + * @default + */ + fillRule: 'nonzero', + + /** + * Composite rule used for canvas globalCompositeOperation + * @type String + * @default + */ + globalCompositeOperation: 'source-over', + + /** + * Background color of an object. + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + backgroundColor: '', + + /** + * Selection Background color of an object. colored layer behind the object when it is active. + * does not mix good with globalCompositeOperation methods. + * @type String + * @default + */ + selectionBackgroundColor: '', + + /** + * When defined, an object is rendered via stroke and this property specifies its color + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + stroke: null, + + /** + * Width of a stroke used to render this object + * @type Number + * @default + */ + strokeWidth: 1, + + /** + * Array specifying dash pattern of an object's stroke (stroke must be defined) + * @type Array + */ + strokeDashArray: null, + + /** + * Line offset of an object's stroke + * @type Number + * @default + */ + strokeDashOffset: 0, + + /** + * Line endings style of an object's stroke (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'butt', + + /** + * Corner style of an object's stroke (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'miter', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke + * @type Number + * @default + */ + strokeMiterLimit: 4, + + /** + * Shadow object representing shadow of this shape + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Opacity of object's controlling borders when object is active and moving + * @type Number + * @default + */ + borderOpacityWhenMoving: 0.4, + + /** + * Scale factor of object's controlling borders + * bigger number will make a thicker border + * border is 1, so this is basically a border thickness + * since there is no way to change the border itself. + * @type Number + * @default + */ + borderScaleFactor: 1, + + /** + * Minimum allowed scale value of an object + * @type Number + * @default + */ + minScaleLimit: 0, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). + * But events still fire on it. + * @type Boolean + * @default + */ + selectable: true, + + /** + * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 + * @type Boolean + * @default + */ + evented: true, + + /** + * When set to `false`, an object is not rendered on canvas + * @type Boolean + * @default + */ + visible: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @type Boolean + * @default + */ + hasControls: true, + + /** + * When set to `false`, object's controlling borders are not rendered + * @type Boolean + * @default + */ + hasBorders: true, + + /** + * When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * When `false`, default object's values are not included in its serialization + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `true`, object horizontal movement is locked + * @type Boolean + * @default + */ + lockMovementX: false, + + /** + * When `true`, object vertical movement is locked + * @type Boolean + * @default + */ + lockMovementY: false, + + /** + * When `true`, object rotation is locked + * @type Boolean + * @default + */ + lockRotation: false, + + /** + * When `true`, object horizontal scaling is locked + * @type Boolean + * @default + */ + lockScalingX: false, + + /** + * When `true`, object vertical scaling is locked + * @type Boolean + * @default + */ + lockScalingY: false, + + /** + * When `true`, object horizontal skewing is locked + * @type Boolean + * @default + */ + lockSkewingX: false, + + /** + * When `true`, object vertical skewing is locked + * @type Boolean + * @default + */ + lockSkewingY: false, + + /** + * When `true`, object cannot be flipped by scaling into negative values + * @type Boolean + * @default + */ + lockScalingFlip: false, + + /** + * When `true`, object is not exported in OBJECT/JSON + * @since 1.6.3 + * @type Boolean + * @default + */ + excludeFromExport: false, + + /** + * When `true`, object is cached on an additional canvas. + * When `false`, object is not cached unless necessary ( clipPath ) + * default to true + * @since 1.7.0 + * @type Boolean + * @default true + */ + objectCaching: objectCaching, + + /** + * When `true`, object properties are checked for cache invalidation. In some particular + * situation you may want this to be disabled ( spray brush, very big, groups) + * or if your application does not allow you to modify properties for groups child you want + * to disable it for groups. + * default to false + * since 1.7.0 + * @type Boolean + * @default false + */ + statefullCache: false, + + /** + * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled + * too much and will be redrawn with correct details at the end of scaling. + * this setting is performance and application dependant. + * default to true + * since 1.7.0 + * @type Boolean + * @default true + */ + noScaleCache: true, + + /** + * When `false`, the stoke width will scale with the object. + * When `true`, the stroke will always match the exact pixel size entered for stroke width. + * default to false + * @since 2.6.0 + * @type Boolean + * @default false + * @type Boolean + * @default false + */ + strokeUniform: false, + + /** + * When set to `true`, object's cache will be rerendered next render call. + * since 1.7.0 + * @type Boolean + * @default true + */ + dirty: true, + + /** + * keeps the value of the last hovered corner during mouse move. + * 0 is no corner, or 'mt', 'ml', 'mtr' etc.. + * It should be private, but there is no harm in using it as + * a read-only property. + * @type number|string|any + * @default 0 + */ + __corner: 0, + + /** + * Determines if the fill or the stroke is drawn first (one of "fill" or "stroke") + * @type String + * @default + */ + paintFirst: 'fill', + + /** + * When 'down', object is set to active on mousedown/touchstart + * When 'up', object is set to active on mouseup/touchend + * Experimental. Let's see if this breaks anything before supporting officially + * @private + * since 4.4.0 + * @type String + * @default 'down' + */ + activeOn: 'down', + + /** + * List of properties to consider when checking if state + * of an object is changed (fabric.Object#hasStateChanged) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + + 'stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit ' + + 'angle opacity fill globalCompositeOperation shadow visible backgroundColor ' + + 'skewX skewY fillRule paintFirst clipPath strokeUniform' + ).split(' '), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: ( + 'fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform' + + ' strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath' + ).split(' '), + + /** + * List of properties to consider for animating colors. + * @type Array + */ + colorProperties: ( + 'fill stroke backgroundColor' + ).split(' '), + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the object has rendered, and the context is placed in the center + * of the object cacheCanvas. + * If you want 0,0 of a clipPath to align with an object center, use clipPath.originX/Y to 'center' + * @type fabric.Object + */ + clipPath: undefined, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will make the object clip to the outside of the clipPath + * since 2.4.0 + * @type boolean + * @default false + */ + inverted: false, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will have its top and left relative to canvas, and will + * not be influenced by the object transform. This will make the clipPath relative + * to the canvas, but clipping just a particular object. + * WARNING this is beta, this feature may change or be renamed. + * since 2.4.0 + * @type boolean + * @default false + */ + absolutePositioned: false, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Create a the canvas used to keep the cached copy of the object + * @private + */ + _createCacheCanvas: function() { + this._cacheProperties = {}; + this._cacheCanvas = fabric.util.createCanvasElement(); + this._cacheContext = this._cacheCanvas.getContext('2d'); + this._updateCacheCanvas(); + // if canvas gets created, is empty, so dirty. + this.dirty = true; + }, + + /** + * Limit the cache dimensions so that X * Y do not cross fabric.perfLimitSizeTotal + * and each side do not cross fabric.cacheSideLimit + * those numbers are configurable so that you can get as much detail as you want + * making bargain with performances. + * @param {Object} dims + * @param {Object} dims.width width of canvas + * @param {Object} dims.height height of canvas + * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _limitCacheSize: function(dims) { + var perfLimitSizeTotal = fabric.perfLimitSizeTotal, + width = dims.width, height = dims.height, + max = fabric.maxCacheSideLimit, min = fabric.minCacheSideLimit; + if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { + if (width < min) { + dims.width = min; + } + if (height < min) { + dims.height = min; + } + return dims; + } + var ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal), + capValue = fabric.util.capValue, + x = capValue(min, limitedDims.x, max), + y = capValue(min, limitedDims.y, max); + if (width > x) { + dims.zoomX /= width / x; + dims.width = x; + dims.capped = true; + } + if (height > y) { + dims.zoomY /= height / y; + dims.height = y; + dims.capped = true; + } + return dims; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @return {Object}.x width of object to be cached + * @return {Object}.y height of object to be cached + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var objectScale = this.getTotalObjectScaling(), + // caculate dimensions without skewing + dim = this._getTransformedDimensions(0, 0), + neededX = dim.x * objectScale.scaleX / this.scaleX, + neededY = dim.y * objectScale.scaleY / this.scaleY; + return { + // for sure this ALIASING_LIMIT is slightly creating problem + // in situation in which the cache canvas gets an upper limit + // also objectScale contains already scaleX and scaleY + width: neededX + ALIASING_LIMIT, + height: neededY + ALIASING_LIMIT, + zoomX: objectScale.scaleX, + zoomY: objectScale.scaleY, + x: neededX, + y: neededY + }; + }, + + /** + * Update width and height of the canvas for cache + * returns true or false if canvas needed resize. + * @private + * @return {Boolean} true if the canvas has been resized + */ + _updateCacheCanvas: function() { + var targetCanvas = this.canvas; + if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) { + var target = targetCanvas._currentTransform.target, + action = targetCanvas._currentTransform.action; + if (this === target && action.slice && action.slice(0, 5) === 'scale') { + return false; + } + } + var canvas = this._cacheCanvas, + dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + minCacheSize = fabric.minCacheSideLimit, + width = dims.width, height = dims.height, drawingWidth, drawingHeight, + zoomX = dims.zoomX, zoomY = dims.zoomY, + dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight, + zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY, + shouldRedraw = dimensionsChanged || zoomChanged, + additionalWidth = 0, additionalHeight = 0, shouldResizeCanvas = false; + if (dimensionsChanged) { + var canvasWidth = this._cacheCanvas.width, + canvasHeight = this._cacheCanvas.height, + sizeGrowing = width > canvasWidth || height > canvasHeight, + sizeShrinking = (width < canvasWidth * 0.9 || height < canvasHeight * 0.9) && + canvasWidth > minCacheSize && canvasHeight > minCacheSize; + shouldResizeCanvas = sizeGrowing || sizeShrinking; + if (sizeGrowing && !dims.capped && (width > minCacheSize || height > minCacheSize)) { + additionalWidth = width * 0.1; + additionalHeight = height * 0.1; + } + } + if (this instanceof fabric.Text && this.path) { + shouldRedraw = true; + shouldResizeCanvas = true; + additionalWidth += this.getHeightOfLine(0) * this.zoomX; + additionalHeight += this.getHeightOfLine(0) * this.zoomY; + } + if (shouldRedraw) { + if (shouldResizeCanvas) { + canvas.width = Math.ceil(width + additionalWidth); + canvas.height = Math.ceil(height + additionalHeight); + } + else { + this._cacheContext.setTransform(1, 0, 0, 1, 0, 0); + this._cacheContext.clearRect(0, 0, canvas.width, canvas.height); + } + drawingWidth = dims.x / 2; + drawingHeight = dims.y / 2; + this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; + this.cacheTranslationY = Math.round(canvas.height / 2 - drawingHeight) + drawingHeight; + this.cacheWidth = width; + this.cacheHeight = height; + this._cacheContext.translate(this.cacheTranslationX, this.cacheTranslationY); + this._cacheContext.scale(zoomX, zoomY); + this.zoomX = zoomX; + this.zoomY = zoomY; + return true; + } + return false; + }, + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + this._setOptions(options); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); + }, + + /** + * Transforms context when rendering an object + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function(ctx) { + var needFullTransform = (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); + var m = this.calcTransformMatrix(!needFullTransform); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + }, + + /** + * Returns an object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + + object = { + type: this.type, + version: fabric.version, + originX: this.originX, + originY: this.originY, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, + strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), + strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray, + strokeLineCap: this.strokeLineCap, + strokeDashOffset: this.strokeDashOffset, + strokeLineJoin: this.strokeLineJoin, + strokeUniform: this.strokeUniform, + strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.angle, NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, + visible: this.visible, + backgroundColor: this.backgroundColor, + fillRule: this.fillRule, + paintFirst: this.paintFirst, + globalCompositeOperation: this.globalCompositeOperation, + skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS), + }; + + if (this.clipPath && !this.clipPath.excludeFromExport) { + object.clipPath = this.clipPath.toObject(propertiesToInclude); + object.clipPath.inverted = this.clipPath.inverted; + object.clipPath.absolutePositioned = this.clipPath.absolutePositioned; + } + + fabric.util.populateWithProperties(this, object, propertiesToInclude); + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + // will be overwritten by subclasses + return this.toObject(propertiesToInclude); + }, + + /** + * @private + * @param {Object} object + */ + _removeDefaultValues: function(object) { + var prototype = fabric.util.getKlass(object.type).prototype, + stateProperties = prototype.stateProperties; + stateProperties.forEach(function(prop) { + if (prop === 'left' || prop === 'top') { + return; + } + if (object[prop] === prototype[prop]) { + delete object[prop]; + } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + + // basically a check for [] === [] + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + delete object[prop]; + } + }); + + return object; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Return the object scale factor counting also the group scaling + * @return {Object} object with scaleX and scaleY properties + */ + getObjectScaling: function() { + // if the object is a top level one, on the canvas, we go for simple aritmetic + // otherwise the complex method with angles will return approximations and decimals + // and will likely kill the cache when not needed + // https://github.com/fabricjs/fabric.js/issues/7157 + if (!this.group) { + return { + scaleX: this.scaleX, + scaleY: this.scaleY, + }; + } + // if we are inside a group total zoom calculation is complex, we defer to generic matrices + var options = fabric.util.qrDecompose(this.calcTransformMatrix()); + return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; + }, + + /** + * Return the object scale factor counting also the group scaling, zoom and retina + * @return {Object} object with scaleX and scaleY properties + */ + getTotalObjectScaling: function() { + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + if (this.canvas) { + var zoom = this.canvas.getZoom(); + var retina = this.canvas.getRetinaScaling(); + scaleX *= zoom * retina; + scaleY *= zoom * retina; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + + /** + * Return the object opacity counting also the group property + * @return {Number} + */ + getObjectOpacity: function() { + var opacity = this.opacity; + if (this.group) { + opacity *= this.group.getObjectOpacity(); + } + return opacity; + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Object} thisArg + */ + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'), + isChanged = this[key] !== value, groupNeedsUpdate = false; + + if (shouldConstrainValue) { + value = this._constrainScale(value); + } + if (key === 'scaleX' && value < 0) { + this.flipX = !this.flipX; + value *= -1; + } + else if (key === 'scaleY' && value < 0) { + this.flipY = !this.flipY; + value *= -1; + } + else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { + value = new fabric.Shadow(value); + } + else if (key === 'dirty' && this.group) { + this.group.set('dirty', value); + } + + this[key] = value; + + if (isChanged) { + groupNeedsUpdate = this.group && this.group.isOnACache(); + if (this.cacheProperties.indexOf(key) > -1) { + this.dirty = true; + groupNeedsUpdate && this.group.set('dirty', true); + } + else if (groupNeedsUpdate && this.stateProperties.indexOf(key) > -1) { + this.group.set('dirty', true); + } + } + return this; + }, + + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf fabric.Object.prototype + * @return {Array} + */ + getViewportTransform: function() { + if (this.canvas && this.canvas.viewportTransform) { + return this.canvas.viewportTransform; + } + return fabric.iMatrix.concat(); + }, + + /* + * @private + * return if the object would be visible in rendering + * @memberOf fabric.Object.prototype + * @return {Boolean} + */ + isNotVisible: function() { + return this.opacity === 0 || + (!this.width && !this.height && this.strokeWidth === 0) || + !this.visible; + }, + + /** + * Renders an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if width/height are zeros or object is not visible + if (this.isNotVisible()) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + ctx.save(); + this._setupCompositeOperation(ctx); + this.drawSelectionBackground(ctx); + this.transform(ctx); + this._setOpacity(ctx); + this._setShadow(ctx, this); + if (this.shouldCache()) { + this.renderCache(); + this.drawCacheOnCanvas(ctx); + } + else { + this._removeCacheCanvas(); + this.dirty = false; + this.drawObject(ctx); + if (this.objectCaching && this.statefullCache) { + this.saveState({ propertySet: 'cacheProperties' }); + } + } + ctx.restore(); + }, + + renderCache: function(options) { + options = options || {}; + if (!this._cacheCanvas) { + this._createCacheCanvas(); + } + if (this.isCacheDirty()) { + this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, options.forClipping); + this.dirty = false; + } + }, + + /** + * Remove cacheCanvas and its dimensions from the objects + */ + _removeCacheCanvas: function() { + this._cacheCanvas = null; + this.cacheWidth = 0; + this.cacheHeight = 0; + }, + + /** + * return true if the object will draw a stroke + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an approximation and be fast. + * wrote to avoid extra caching, it has to return true when stroke happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the stroke is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasStroke: function() { + return this.stroke && this.stroke !== 'transparent' && this.strokeWidth !== 0; + }, + + /** + * return true if the object will draw a fill + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an approximation and be fast. + * wrote to avoid extra caching, it has to return true when fill happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the fill is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasFill: function() { + return this.fill && this.fill !== 'transparent'; + }, + + /** + * When set to `true`, force the object to have its own cache, even if it is inside a group + * it may be needed when your object behave in a particular way on the cache and always needs + * its own isolated canvas to render correctly. + * Created to be overridden + * since 1.7.12 + * @returns Boolean + */ + needsItsOwnCache: function() { + if (this.paintFirst === 'stroke' && + this.hasFill() && this.hasStroke() && typeof this.shadow === 'object') { + return true; + } + if (this.clipPath) { + return true; + } + return false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * Read as: cache if is needed, or if the feature is enabled but we are not already caching. + * @return {Boolean} + */ + shouldCache: function() { + this.ownCaching = this.needsItsOwnCache() || ( + this.objectCaching && + (!this.group || !this.group.isOnACache()) + ); + return this.ownCaching; + }, + + /** + * Check if this object or a child object will cast a shadow + * used by Group.shouldCache to know if child has a shadow recursively + * @return {Boolean} + */ + willDrawShadow: function() { + return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); + }, + + /** + * Execute the drawing operation for an object clipPath + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCache: function(ctx) { + var path = this.clipPath; + ctx.save(); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4 + if (path.inverted) { + ctx.globalCompositeOperation = 'destination-out'; + } + else { + ctx.globalCompositeOperation = 'destination-in'; + } + //ctx.scale(1 / 2, 1 / 2); + if (path.absolutePositioned) { + var m = fabric.util.invertTransform(this.calcTransformMatrix()); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx, forClipping) { + var originalFill = this.fill, originalStroke = this.stroke; + if (forClipping) { + this.fill = 'black'; + this.stroke = ''; + this._setClippingProperties(ctx); + } + else { + this._renderBackground(ctx); + } + this._render(ctx); + this._drawClipPath(ctx); + this.fill = originalFill; + this.stroke = originalStroke; + }, + + _drawClipPath: function(ctx) { + var path = this.clipPath; + if (!path) { return; } + // needed to setup a couple of variables + // path canvas gets overridden with this one. + // TODO find a better solution? + path.canvas = this.canvas; + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCache(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + ctx.scale(1 / this.zoomX, 1 / this.zoomY); + ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); + }, + + /** + * Check if cache is dirty + * @param {Boolean} skipCanvas skip canvas checks because this object is painted + * on parent canvas. + */ + isCacheDirty: function(skipCanvas) { + if (this.isNotVisible()) { + return false; + } + if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { + // in this case the context is already cleared. + return true; + } + else { + if (this.dirty || + (this.clipPath && this.clipPath.absolutePositioned) || + (this.statefullCache && this.hasStateChanged('cacheProperties')) + ) { + if (this._cacheCanvas && !skipCanvas) { + var width = this.cacheWidth / this.zoomX; + var height = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-width / 2, -height / 2, width, height); + } + return true; + } + } + return false; + }, + + /** + * Draws a background for the object big as its untransformed dimensions + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (!this.backgroundColor) { + return; + } + var dim = this._getNonTransformedDimensions(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + -dim.x / 2, + -dim.y / 2, + dim.x, + dim.y + ); + // if there is background color no other shadows + // should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setOpacity: function(ctx) { + if (this.group && !this.group._transformDone) { + ctx.globalAlpha = this.getObjectOpacity(); + } + else { + ctx.globalAlpha *= this.opacity; + } + }, + + _setStrokeStyles: function(ctx, decl) { + var stroke = decl.stroke; + if (stroke) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = decl.strokeLineCap; + ctx.lineDashOffset = decl.strokeDashOffset; + ctx.lineJoin = decl.strokeLineJoin; + ctx.miterLimit = decl.strokeMiterLimit; + if (stroke.toLive) { + if (stroke.gradientUnits === 'percentage' || stroke.gradientTransform || stroke.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + this._applyPatternForTransformedGradient(ctx, stroke); + } + else { + // is a simple gradient or pattern + ctx.strokeStyle = stroke.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, stroke); + } + } + else { + // is a color + ctx.strokeStyle = decl.stroke; + } + } + }, + + _setFillStyles: function(ctx, decl) { + var fill = decl.fill; + if (fill) { + if (fill.toLive) { + ctx.fillStyle = fill.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, decl.fill); + } + else { + ctx.fillStyle = fill; + } + } + }, + + _setClippingProperties: function(ctx) { + ctx.globalAlpha = 1; + ctx.strokeStyle = 'transparent'; + ctx.fillStyle = '#000000'; + }, + + /** + * @private + * Sets line dash + * @param {CanvasRenderingContext2D} ctx Context to set the dash line on + * @param {Array} dashArray array representing dashes + */ + _setLineDash: function(ctx, dashArray) { + if (!dashArray || dashArray.length === 0) { + return; + } + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & dashArray.length) { + dashArray.push.apply(dashArray, dashArray); + } + ctx.setLineDash(dashArray); + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + */ + _renderControls: function(ctx, styleOverride) { + var vpt = this.getViewportTransform(), + matrix = this.calcTransformMatrix(), + options, drawBorders, drawControls; + styleOverride = styleOverride || { }; + drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders; + drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls; + matrix = fabric.util.multiplyTransformMatrices(vpt, matrix); + options = fabric.util.qrDecompose(matrix); + ctx.save(); + ctx.translate(options.translateX, options.translateY); + ctx.lineWidth = 1 * this.borderScaleFactor; + if (!this.group) { + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + } + ctx.rotate(degreesToRadians(options.angle)); + if (styleOverride.forActiveSelection || this.group) { + drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); + } + else { + drawBorders && this.drawBorders(ctx, styleOverride); + } + drawControls && this.drawControls(ctx, styleOverride); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setShadow: function(ctx) { + if (!this.shadow) { + return; + } + + var shadow = this.shadow, canvas = this.canvas, scaling, + multX = (canvas && canvas.viewportTransform[0]) || 1, + multY = (canvas && canvas.viewportTransform[3]) || 1; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + if (canvas && canvas._isRetinaScaling()) { + multX *= fabric.devicePixelRatio; + multY *= fabric.devicePixelRatio; + } + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * + (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _removeShadow: function(ctx) { + if (!this.shadow) { + return; + } + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} filler fabric.Pattern or fabric.Gradient + * @return {Object} offset.offsetX offset for text rendering + * @return {Object} offset.offsetY offset for text rendering + */ + _applyPatternGradientTransform: function(ctx, filler) { + if (!filler || !filler.toLive) { + return { offsetX: 0, offsetY: 0 }; + } + var t = filler.gradientTransform || filler.patternTransform; + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; + + if (filler.gradientUnits === 'percentage') { + ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); + } + else { + ctx.transform(1, 0, 0, 1, offsetX, offsetY); + } + if (t) { + ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); + } + return { offsetX: offsetX, offsetY: offsetY }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderPaintInOrder: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderStroke(ctx); + this._renderFill(ctx); + } + else { + this._renderFill(ctx); + this._renderStroke(ctx); + } + }, + + /** + * @private + * function that actually render something on the context. + * empty here to allow Obects to work on tests to benchmark fabric functionalites + * not related to rendering + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(/* ctx */) { + + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderFill: function(ctx) { + if (!this.fill) { + return; + } + + ctx.save(); + this._setFillStyles(ctx, this); + if (this.fillRule === 'evenodd') { + ctx.fill('evenodd'); + } + else { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderStroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + if (this.strokeUniform && this.group) { + var scaling = this.getObjectScaling(); + ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); + } + else if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); + } + this._setLineDash(ctx, this.strokeDashArray); + this._setStrokeStyles(ctx, this); + ctx.stroke(); + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drwabacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Gradient} filler a fabric gradient instance + */ + _applyPatternForTransformedGradient: function(ctx, filler) { + var dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + pCanvas = fabric.util.createCanvasElement(), pCtx, retinaScaling = this.canvas.getRetinaScaling(), + width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.scale( + dims.zoomX / this.scaleX / retinaScaling, + dims.zoomY / this.scaleY / retinaScaling + ); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fillStyle = filler.toLive(ctx); + pCtx.fill(); + ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); + ctx.scale( + retinaScaling * this.scaleX / dims.zoomX, + retinaScaling * this.scaleY / dims.zoomY + ); + ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { x: this.left + this.width / 2, y: this.top + this.height / 2 }; + }, + + /** + * This function is an helper for svg import. it decompose the transformMatrix + * and assign properties to object. + * untransformed coordinates + * @private + * @chainable + */ + _assignTransformMatrixProps: function() { + if (this.transformMatrix) { + var options = fabric.util.qrDecompose(this.transformMatrix); + this.flipX = false; + this.flipY = false; + this.set('scaleX', options.scaleX); + this.set('scaleY', options.scaleY); + this.angle = options.angle; + this.skewX = options.skewX; + this.skewY = 0; + } + }, + + /** + * This function is an helper for svg import. it removes the transform matrix + * and set to object properties that fabricjs can handle + * @private + * @param {Object} preserveAspectRatioOptions + * @return {thisArg} + */ + _removeTransformMatrix: function(preserveAspectRatioOptions) { + var center = this._findCenterFromElement(); + if (this.transformMatrix) { + this._assignTransformMatrixProps(); + center = fabric.util.transformPoint(center, this.transformMatrix); + } + this.transformMatrix = null; + if (preserveAspectRatioOptions) { + this.scaleX *= preserveAspectRatioOptions.scaleX; + this.scaleY *= preserveAspectRatioOptions.scaleY; + this.cropX = preserveAspectRatioOptions.cropX; + this.cropY = preserveAspectRatioOptions.cropY; + center.x += preserveAspectRatioOptions.offsetLeft; + center.y += preserveAspectRatioOptions.offsetTop; + this.width = preserveAspectRatioOptions.width; + this.height = preserveAspectRatioOptions.height; + } + this.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * Clones an instance, using a callback method will work for every object. + * @param {Function} callback Callback is invoked with a clone as a first argument + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + */ + clone: function(callback, propertiesToInclude) { + var objectForm = this.toObject(propertiesToInclude); + if (this.constructor.fromObject) { + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); + } + }, + + /** + * Creates an instance of fabric.Image out of an object + * makes use of toCanvasElement. + * Once this method was based on toDataUrl and loadImage, so it also had a quality + * and format option. toCanvasElement is faster and produce no loss of quality. + * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. + * toCanvasElement and then toBlob from the obtained canvas is also a good option. + * This method is sync now, but still support the callback because we did not want to break. + * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. + * @param {Function} callback callback, invoked with an instance as a first argument + * @param {Object} [options] for clone as image, passed to toDataURL + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {fabric.Object} thisArg + */ + cloneAsImage: function(callback, options) { + var canvasEl = this.toCanvasElement(options); + if (callback) { + callback(new fabric.Image(canvasEl)); + } + return this; + }, + + /** + * Converts an object into a HTMLCanvas element + * @param {Object} options Options object + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {HTMLCanvasElement} Returns DOM element with the fabric.Object + */ + toCanvasElement: function(options) { + options || (options = { }); + + var utils = fabric.util, origParams = utils.saveObjectTransform(this), + originalGroup = this.group, + originalShadow = this.shadow, abs = Math.abs, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); + delete this.group; + if (options.withoutTransform) { + utils.resetObjectTransform(this); + } + if (options.withoutShadow) { + this.shadow = null; + } + + var el = fabric.util.createCanvasElement(), + // skip canvas zoom and calculate with setCoords now. + boundingRect = this.getBoundingRect(true, true), + shadow = this.shadow, scaling, + shadowOffset = { x: 0, y: 0 }, shadowBlur, + width, height; + + if (shadow) { + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + // consider non scaling shadow. + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + } + width = boundingRect.width + shadowOffset.x; + height = boundingRect.height + shadowOffset.y; + // if the current width/height is not an integer + // we need to make it so. + el.width = Math.ceil(width); + el.height = Math.ceil(height); + var canvas = new fabric.StaticCanvas(el, { + enableRetinaScaling: false, + renderOnAddRemove: false, + skipOffscreen: false, + }); + if (options.format === 'jpeg') { + canvas.backgroundColor = '#fff'; + } + this.setPositionByOrigin(new fabric.Point(canvas.width / 2, canvas.height / 2), 'center', 'center'); + + var originalCanvas = this.canvas; + canvas.add(this); + var canvasEl = canvas.toCanvasElement(multiplier || 1, options); + this.shadow = originalShadow; + this.set('canvas', originalCanvas); + if (originalGroup) { + this.group = originalGroup; + } + this.set(origParams).setCoords(); + // canvas.dispose will call image.dispose that will nullify the elements + // since this canvas is a simple element for the process, we remove references + // to objects in this way in order to avoid object trashing. + canvas._objects = []; + canvas.dispose(); + canvas = null; + + return canvasEl; + }, + + /** + * Converts an object into a data-url-like string + * @param {Object} options Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toDataURL: function(options) { + options || (options = { }); + return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); + }, + + /** + * Returns true if specified type is identical to the type of an instance + * @param {String} type Type to check against + * @return {Boolean} + */ + isType: function(type) { + return this.type === type; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance (is 1 unless subclassed) + */ + complexity: function() { + return 1; + }, + + /** + * Returns a JSON representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON + */ + toJSON: function(propertiesToInclude) { + // delegate, not alias + return this.toObject(propertiesToInclude); + }, + + /** + * Sets "angle" of an instance with centered rotation + * @param {Number} angle Angle value (in degrees) + * @return {fabric.Object} thisArg + * @chainable + */ + rotate: function(angle) { + var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; + + if (shouldCenterOrigin) { + this._setOriginToCenter(); + } + + this.set('angle', angle); + + if (shouldCenterOrigin) { + this._resetOrigin(); + } + + return this; + }, + + /** + * Centers object horizontally on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerH: function () { + this.canvas && this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object horizontally on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterH: function () { + this.canvas && this.canvas.viewportCenterObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas && this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterV: function () { + this.canvas && this.canvas.viewportCenterObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + this.canvas && this.canvas.centerObject(this); + return this; + }, + + /** + * Centers object on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenter: function () { + this.canvas && this.canvas.viewportCenterObject(this); + return this; + }, + + /** + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; + }, + + /** + * Sets canvas globalCompositeOperation for specific object + * custom composition operation for the particular object can be specified using globalCompositeOperation property + * @param {CanvasRenderingContext2D} ctx Rendering canvas context + */ + _setupCompositeOperation: function (ctx) { + if (this.globalCompositeOperation) { + ctx.globalCompositeOperation = this.globalCompositeOperation; + } + } + }); + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object); + + extend(fabric.Object.prototype, fabric.Observable); + + /** + * Defines the number of fraction digits to use when serializing object values. + * You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc. + * @static + * @memberOf fabric.Object + * @constant + * @type Number + */ + fabric.Object.NUM_FRACTION_DIGITS = 2; + + fabric.Object._fromObject = function(className, object, callback, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); + }); + }; + + /** + * Unique id used internally when creating SVG elements + * @static + * @memberOf fabric.Object + * @type Number + */ + fabric.Object.__uid = 0; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + originXOffset = { + left: -0.5, + center: 0, + right: 0.5 + }, + originYOffset = { + top: -0.5, + center: 0, + bottom: 0.5 + }; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Translates the coordinates from a set of origin to another (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { + var x = point.x, + y = point.y, + offsetX, offsetY, dim; + + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; + } + + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } + + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; + + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; + } + + return new fabric.Point(x, y); + }, + + /** + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToCenterPoint: function(point, originX, originY) { + var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); + if (this.angle) { + return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {fabric.Point} center The point which corresponds to center of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToOriginPoint: function(center, originX, originY) { + var p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + if (this.angle) { + return fabric.util.rotatePoint(p, center, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Returns the real center coordinates of the object + * @return {fabric.Point} + */ + getCenterPoint: function() { + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); + }, + + /** + * Returns the coordinates of the object based on center coordinates + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @return {fabric.Point} + */ + // getOriginPoint: function(center) { + // return this.translateToOriginPoint(center, this.originX, this.originY); + // }, + + /** + * Returns the coordinates of the object as if it has a different origin + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + getPointByOrigin: function(originX, originY) { + var center = this.getCenterPoint(); + return this.translateToOriginPoint(center, originX, originY); + }, + + /** + * Returns the point in local coordinates + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { + p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + } + else { + p = new fabric.Point(this.left, this.top); + } + + p2 = new fabric.Point(point.x, point.y); + if (this.angle) { + p2 = fabric.util.rotatePoint(p2, center, -degreesToRadians(this.angle)); + } + return p2.subtractEquals(p); + }, + + /** + * Returns the point in global coordinates + * @param {fabric.Point} The point relative to the local coordinate system + * @return {fabric.Point} + */ + // toGlobalPoint: function(point) { + // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); + // }, + + /** + * Sets the position of the object taking into consideration the object's origin + * @param {fabric.Point} pos The new position of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {void} + */ + setPositionByOrigin: function(pos, originX, originY) { + var center = this.translateToCenterPoint(pos, originX, originY), + position = this.translateToOriginPoint(center, this.originX, this.originY); + this.set('left', position.x); + this.set('top', position.y); + }, + + /** + * @param {String} to One of 'left', 'center', 'right' + */ + adjustPosition: function(to) { + var angle = degreesToRadians(this.angle), + hypotFull = this.getScaledWidth(), + xFull = fabric.util.cos(angle) * hypotFull, + yFull = fabric.util.sin(angle) * hypotFull, + offsetFrom, offsetTo; + + //TODO: this function does not consider mixed situation like top, center. + if (typeof this.originX === 'string') { + offsetFrom = originXOffset[this.originX]; + } + else { + offsetFrom = this.originX - 0.5; + } + if (typeof to === 'string') { + offsetTo = originXOffset[to]; + } + else { + offsetTo = to - 0.5; + } + this.left += xFull * (offsetTo - offsetFrom); + this.top += yFull * (offsetTo - offsetFrom); + this.setCoords(); + this.originX = to; + }, + + /** + * Sets the origin/position of the object to it's center point + * @private + * @return {void} + */ + _setOriginToCenter: function() { + this._originalOriginX = this.originX; + this._originalOriginY = this.originY; + + var center = this.getCenterPoint(); + + this.originX = 'center'; + this.originY = 'center'; + + this.left = center.x; + this.top = center.y; + }, + + /** + * Resets the origin/position of the object to it's original origin + * @private + * @return {void} + */ + _resetOrigin: function() { + var originPoint = this.translateToOriginPoint( + this.getCenterPoint(), + this._originalOriginX, + this._originalOriginY); + + this.originX = this._originalOriginX; + this.originY = this._originalOriginY; + + this.left = originPoint.x; + this.top = originPoint.y; + + this._originalOriginX = null; + this._originalOriginY = null; + }, + + /** + * @private + */ + _getLeftTopCoords: function() { + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + }, + }); + +})(); + + +(function() { + + function arrayFromCoords(coords) { + return [ + new fabric.Point(coords.tl.x, coords.tl.y), + new fabric.Point(coords.tr.x, coords.tr.y), + new fabric.Point(coords.br.x, coords.br.y), + new fabric.Point(coords.bl.x, coords.bl.y) + ]; + } + + var util = fabric.util, + degreesToRadians = util.degreesToRadians, + multiplyMatrices = util.multiplyTransformMatrices, + transformPoint = util.transformPoint; + + util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Describe object's corner position in canvas element coordinates. + * properties are depending on control keys and padding the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from the controls positionHandler and are used + * to draw and locate controls + * @memberOf fabric.Object.prototype + */ + oCoords: null, + + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are useful to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcACoords(); + * @memberOf fabric.Object.prototype + */ + aCoords: null, + + /** + * Describe object's corner position in canvas element coordinates. + * includes padding. Used of object detection. + * set and refreshed with setCoords and calcCoords. + * @memberOf fabric.Object.prototype + */ + lineCoords: null, + + /** + * storage for object transform matrix + */ + ownMatrixCache: null, + + /** + * storage for object full transform matrix + */ + matrixCache: null, + + /** + * custom controls interface + * controls are added by default_controls.js + */ + controls: { }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * @param {Boolean} absolute will return aCoords if true or lineCoords + * @return {Object} {tl, tr, br, bl} points + */ + _getCoords: function(absolute, calculate) { + if (calculate) { + return (absolute ? this.calcACoords() : this.calcLineCoords()); + } + if (!this.aCoords || !this.lineCoords) { + this.setCoords(true); + } + return (absolute ? this.aCoords : this.lineCoords); + }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * The coords are returned in an array. + * @return {Array} [tl, tr, br, bl] of points + */ + getCoords: function(absolute, calculate) { + return arrayFromCoords(this._getCoords(absolute, calculate)); + }, + + /** + * Checks if object intersects with an area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect: function(pointTL, pointBR, absolute, calculate) { + var coords = this.getCoords(absolute, calculate), + intersection = fabric.Intersection.intersectPolygonRectangle( + coords, + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject: function(other, absolute, calculate) { + var intersection = fabric.Intersection.intersectPolygonPolygon( + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); + + return intersection.status === 'Intersection' + || other.isContainedWithinObject(this, absolute, calculate) + || this.isContainedWithinObject(other, absolute, calculate); + }, + + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject: function(other, absolute, calculate) { + var points = this.getCoords(absolute, calculate), + otherCoords = absolute ? other.aCoords : other.lineCoords, + i = 0, lines = other._getImageLines(otherCoords); + for (; i < 4; i++) { + if (!other.containsPoint(points[i], lines)) { + return false; + } + } + return true; + }, + + /** + * Checks if object is fully contained within area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect: function(pointTL, pointBR, absolute, calculate) { + var boundingRect = this.getBoundingRect(absolute, calculate); + + return ( + boundingRect.left >= pointTL.x && + boundingRect.left + boundingRect.width <= pointBR.x && + boundingRect.top >= pointTL.y && + boundingRect.top + boundingRect.height <= pointBR.y + ); + }, + + /** + * Checks if point is inside the object + * @param {fabric.Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if point is inside the object + */ + containsPoint: function(point, lines, absolute, calculate) { + var coords = this._getCoords(absolute, calculate), + lines = lines || this._getImageLines(coords), + xPoints = this._findCrossPoints(point, lines); + // if xPoints is odd then point is inside the object + return (xPoints !== 0 && xPoints % 2 === 1); + }, + + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appears on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords + * @return {Boolean} true if object is fully or partially contained within canvas + */ + isOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + var points = this.getCoords(true, calculate); + // if some point is on screen, the object is on screen. + if (points.some(function(point) { + return point.x <= pointBR.x && point.x >= pointTL.x && + point.y <= pointBR.y && point.y >= pointTL.y; + })) { + return true; + } + // no points on screen, check intersection with absolute coordinates + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + return this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Checks if the object contains the midpoint between canvas extremities + * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen + * @private + * @param {Fabric.Point} pointTL Top Left point + * @param {Fabric.Point} pointBR Top Right point + * @param {Boolean} calculate use coordinates of current position instead of .oCoords + * @return {Boolean} true if the object contains the point + */ + _containsCenterOfCanvas: function(pointTL, pointBR, calculate) { + // worst case scenario the object is so big that contains the screen + var centerPoint = { x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2 }; + if (this.containsPoint(centerPoint, null, true, calculate)) { + return true; + } + return false; + }, + + /** + * Checks if object is partially contained within the canvas with current viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is partially contained within canvas + */ + isPartiallyOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + var allPointsAreOutside = this.getCoords(true, calculate).every(function(point) { + return (point.x >= pointBR.x || point.x <= pointTL.x) && + (point.y >= pointBR.y || point.y <= pointTL.y); + }); + return allPointsAreOutside && this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} oCoords Coordinates of the object corners + */ + _getImageLines: function(oCoords) { + + var lines = { + topline: { + o: oCoords.tl, + d: oCoords.tr + }, + rightline: { + o: oCoords.tr, + d: oCoords.br + }, + bottomline: { + o: oCoords.br, + d: oCoords.bl + }, + leftline: { + o: oCoords.bl, + d: oCoords.tl + } + }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; + }, + + /** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {fabric.Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + */ + // remove yi, not used but left code here just in case. + _findCrossPoints: function(point, lines) { + var b1, b2, a1, a2, xi, // yi, + xcount = 0, + iLine; + + for (var lineKey in lines) { + iLine = lines[lineKey]; + // optimisation 1: line below point. no cross + if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { + continue; + } + // optimisation 2: line above point. no cross + if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { + xi = iLine.o.x; + // yi = point.y; + } + // calculate the intersection point + else { + b1 = 0; + b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + a1 = point.y - b1 * point.x; + a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + // yi = a1 + b1 * xi; + } + // dont count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimisation 4: specific for square images + if (xcount === 2) { + break; + } + } + return xcount; + }, + + /** + * Returns coordinates of object's bounding rectangle (left, top, width, height) + * the box is intended as aligned to axis of canvas. + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect: function(absolute, calculate) { + var coords = this.getCoords(absolute, calculate); + return util.makeBoundingBoxFromPoints(coords); + }, + + /** + * Returns width of an object's bounding box counting transformations + * before 2.0 it was named getWidth(); + * @return {Number} width value + */ + getScaledWidth: function() { + return this._getTransformedDimensions().x; + }, + + /** + * Returns height of an object bounding box counting transformations + * before 2.0 it was named getHeight(); + * @return {Number} height value + */ + getScaledHeight: function() { + return this._getTransformedDimensions().y; + }, + + /** + * Makes sure the scale is valid and modifies it if necessary + * @private + * @param {Number} value + * @return {Number} + */ + _constrainScale: function(value) { + if (Math.abs(value) < this.minScaleLimit) { + if (value < 0) { + return -this.minScaleLimit; + } + else { + return this.minScaleLimit; + } + } + else if (value === 0) { + return 0.0001; + } + return value; + }, + + /** + * Scales an object (equally by x and y) + * @param {Number} value Scale factor + * @return {fabric.Object} thisArg + * @chainable + */ + scale: function(value) { + this._set('scaleX', value); + this._set('scaleY', value); + return this.setCoords(); + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New width value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).width / this.getScaledWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New height value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Calculates and returns the .coords of an object. + * unused by the library, only for the end dev. + * @return {Object} Object with tl, tr, br, bl .... + * @chainable + * @deprecated + */ + calcCoords: function(absolute) { + // this is a compatibility function to avoid removing calcCoords now. + if (absolute) { + return this.calcACoords(); + } + return this.calcOCoords(); + }, + + calcLineCoords: function() { + var vpt = this.getViewportTransform(), + padding = this.padding, angle = degreesToRadians(this.angle), + cos = util.cos(angle), sin = util.sin(angle), + cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, + cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); + + var lineCoords = { + tl: transformPoint(aCoords.tl, vpt), + tr: transformPoint(aCoords.tr, vpt), + bl: transformPoint(aCoords.bl, vpt), + br: transformPoint(aCoords.br, vpt), + }; + + if (padding) { + lineCoords.tl.x -= cosPMinusSinP; + lineCoords.tl.y -= cosPSinP; + lineCoords.tr.x += cosPSinP; + lineCoords.tr.y -= cosPMinusSinP; + lineCoords.bl.x -= cosPSinP; + lineCoords.bl.y += cosPMinusSinP; + lineCoords.br.x += cosPMinusSinP; + lineCoords.br.y += cosPSinP; + } + + return lineCoords; + }, + + calcOCoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + vpt = this.getViewportTransform(), + startMatrix = multiplyMatrices(vpt, translateMatrix), + finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), + finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), + dim = this._calculateCurrentDimensions(), + coords = {}; + this.forEachControl(function(control, key, fabricObject) { + coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); + }); + + // debug code + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // Object.keys(coords).forEach(function(key) { + // var control = coords[key]; + // canvas.contextTop.fillRect(control.x, control.y, 3, 3); + // }); + // }, 50); + return coords; + }, + + calcACoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + finalMatrix = multiplyMatrices(translateMatrix, rotateMatrix), + dim = this._getTransformedDimensions(), + w = dim.x / 2, h = dim.y / 2; + return { + // corners + tl: transformPoint({ x: -w, y: -h }, finalMatrix), + tr: transformPoint({ x: w, y: -h }, finalMatrix), + bl: transformPoint({ x: -w, y: h }, finalMatrix), + br: transformPoint({ x: w, y: h }, finalMatrix) + }; + }, + + /** + * Sets corner and controls position coordinates based on current angle, width and height, left and top. + * oCoords are used to find the corners + * aCoords are used to quickly find an object on the canvas + * lineCoords are used to quickly find object during pointer events. + * See {@link https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords|When-to-call-setCoords} + * @param {Boolean} [skipCorners] skip calculation of oCoords. + * @return {fabric.Object} thisArg + * @chainable + */ + setCoords: function(skipCorners) { + this.aCoords = this.calcACoords(); + // in case we are in a group, for how the inner group target check works, + // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. + this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); + if (skipCorners) { + return this; + } + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this.oCoords = this.calcOCoords(); + this._setCornerCoords && this._setCornerCoords(); + return this; + }, + + /** + * calculate rotation matrix of an object + * @return {Array} rotation matrix for the object + */ + _calcRotateMatrix: function() { + return util.calcRotateMatrix(this); + }, + + /** + * calculate the translation matrix for an object transform + * @return {Array} rotation matrix for the object + */ + _calcTranslateMatrix: function() { + var center = this.getCenterPoint(); + return [1, 0, 0, 1, center.x, center.y]; + }, + + transformMatrixKey: function(skipGroup) { + var sep = '_', prefix = ''; + if (!skipGroup && this.group) { + prefix = this.group.transformMatrixKey(skipGroup) + sep; + }; + return prefix + this.top + sep + this.left + sep + this.scaleX + sep + this.scaleY + + sep + this.skewX + sep + this.skewY + sep + this.angle + sep + this.originX + sep + this.originY + + sep + this.width + sep + this.height + sep + this.strokeWidth + this.flipX + this.flipY; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties. + * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations + * There are some situation in which this is useful to avoid the fake rotation. + * @return {Array} transform matrix for the object + */ + calcTransformMatrix: function(skipGroup) { + var matrix = this.calcOwnMatrix(); + if (skipGroup || !this.group) { + return matrix; + } + var key = this.transformMatrixKey(skipGroup), cache = this.matrixCache || (this.matrixCache = {}); + if (cache.key === key) { + return cache.value; + } + if (this.group) { + matrix = multiplyMatrices(this.group.calcTransformMatrix(false), matrix); + } + cache.key = key; + cache.value = matrix; + return matrix; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {Array} transform matrix for the object + */ + calcOwnMatrix: function() { + var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); + if (cache.key === key) { + return cache.value; + } + var tMatrix = this._calcTranslateMatrix(), + options = { + angle: this.angle, + translateX: tMatrix[4], + translateY: tMatrix[5], + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + flipX: this.flipX, + flipY: this.flipY, + }; + cache.key = key; + cache.value = util.composeMatrix(options); + return cache.value; + }, + + /* + * Calculate object dimensions from its properties + * @private + * @deprecated since 3.4.0, please use fabric.util._calcDimensionsTransformMatrix + * not including or including flipX, flipY to emulate the flipping boolean + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { + return util.calcDimensionsMatrix({ + skewX: skewX, + skewY: skewY, + scaleX: this.scaleX * (flipping && this.flipX ? -1 : 1), + scaleY: this.scaleY * (flipping && this.flipY ? -1 : 1) + }); + }, + + /* + * Calculate object dimensions from its properties + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getNonTransformedDimensions: function() { + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param {Number} skewX, a value to override current skewX + * @param {Number} skewY, a value to override current skewY + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions, dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + + if (this.strokeUniform) { + dimX = this.width; + dimY = this.height; + } + else { + dimensions = this._getNonTransformedDimensions(); + dimX = dimensions.x; + dimY = dimensions.y; + } + if (noSkew) { + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + } + var bbox = util.sizeAfterTransform(dimX, dimY, { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: skewX, + skewY: skewY, + }); + return this._finalizeDimensions(bbox.x, bbox.y); + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param Number width width of the bbox + * @param Number height height of the bbox + * @private + * @return {Object} .x finalized width dimension + * @return {Object} .y finalized height dimension + */ + _finalizeDimensions: function(width, height) { + return this.strokeUniform ? + { x: width + this.strokeWidth, y: height + this.strokeWidth } + : + { x: width, y: height }; + }, + + /* + * Calculate object dimensions for controls box, including padding and canvas zoom. + * and active selection + * private + */ + _calculateCurrentDimensions: function() { + var vpt = this.getViewportTransform(), + dim = this._getTransformedDimensions(), + p = transformPoint(dim, vpt, true); + return p.scalarAdd(2 * this.padding); + }, + }); +})(); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else if (this.canvas) { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else if (this.canvas) { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else if (this.canvas) { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else if (this.canvas) { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group && this.group.type !== 'activeSelection') { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else if (this.canvas) { + this.canvas.moveTo(this, index); + } + return this; + } +}); + + +/* _TO_SVG_START_ */ +(function() { + function getSvgColorString(prop, value) { + if (!value) { + return prop + ': none; '; + } + else if (value.toLive) { + return prop + ': url(#SVGID_' + value.id + '); '; + } + else { + var color = new fabric.Color(value), + str = prop + ': ' + color.toRgb() + '; ', + opacity = color.getAlpha(); + if (opacity !== 1) { + //change the color in rgb + opacity + str += prop + '-opacity: ' + opacity.toString() + '; '; + } + return str; + } + } + + var toFixed = fabric.util.toFixed; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + + var fillRule = this.fillRule ? this.fillRule : 'nonzero', + strokeWidth = this.strokeWidth ? this.strokeWidth : '0', + strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none', + strokeDashOffset = this.strokeDashOffset ? this.strokeDashOffset : '0', + strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', + strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', + strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', + opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', + visibility = this.visible ? '' : ' visibility: hidden;', + filter = skipShadow ? '' : this.getSvgFilter(), + fill = getSvgColorString('fill', this.fill), + stroke = getSvgColorString('stroke', this.stroke); + + return [ + stroke, + 'stroke-width: ', strokeWidth, '; ', + 'stroke-dasharray: ', strokeDashArray, '; ', + 'stroke-linecap: ', strokeLineCap, '; ', + 'stroke-dashoffset: ', strokeDashOffset, '; ', + 'stroke-linejoin: ', strokeLineJoin, '; ', + 'stroke-miterlimit: ', strokeMiterLimit, '; ', + fill, + 'fill-rule: ', fillRule, '; ', + 'opacity: ', opacity, ';', + filter, + visibility + ].join(''); + }, + + /** + * Returns styles-string for svg-export + * @param {Object} style the object from which to retrieve style properties + * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. + * @return {String} + */ + getSvgSpanStyles: function(style, useWhiteSpace) { + var term = '; '; + var fontFamily = style.fontFamily ? + 'font-family: ' + (((style.fontFamily.indexOf('\'') === -1 && style.fontFamily.indexOf('"') === -1) ? + '\'' + style.fontFamily + '\'' : style.fontFamily)) + term : ''; + var strokeWidth = style.strokeWidth ? 'stroke-width: ' + style.strokeWidth + term : '', + fontFamily = fontFamily, + fontSize = style.fontSize ? 'font-size: ' + style.fontSize + 'px' + term : '', + fontStyle = style.fontStyle ? 'font-style: ' + style.fontStyle + term : '', + fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + term : '', + fill = style.fill ? getSvgColorString('fill', style.fill) : '', + stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '', + textDecoration = this.getSvgTextDecoration(style), + deltaY = style.deltaY ? 'baseline-shift: ' + (-style.deltaY) + '; ' : ''; + if (textDecoration) { + textDecoration = 'text-decoration: ' + textDecoration + term; + } + + return [ + stroke, + strokeWidth, + fontFamily, + fontSize, + fontStyle, + fontWeight, + textDecoration, + fill, + deltaY, + useWhiteSpace ? 'white-space: pre; ' : '' + ].join(''); + }, + + /** + * Returns text-decoration property for svg-export + * @param {Object} style the object from which to retrieve style properties + * @return {String} + */ + getSvgTextDecoration: function(style) { + return ['overline', 'underline', 'line-through'].filter(function(decoration) { + return style[decoration.replace('-', '')]; + }).join(' '); + }, + + /** + * Returns filter for svg shadow + * @return {String} + */ + getSvgFilter: function() { + return this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + }, + + /** + * Returns id attribute for svg output + * @return {String} + */ + getSvgCommons: function() { + return [ + this.id ? 'id="' + this.id + '" ' : '', + this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '', + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @param {Boolean} use the full transform or the single object one. + * @return {String} + */ + getSvgTransform: function(full, additionalTransform) { + var transform = full ? this.calcTransformMatrix() : this.calcOwnMatrix(), + svgTransform = 'transform="' + fabric.util.matrixToSVG(transform); + return svgTransform + + (additionalTransform || '') + '" '; + }, + + _setSVGBg: function(textBgRects) { + if (this.backgroundColor) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + } + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + return this._createBaseSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + return '\t' + this._createBaseClipPathSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * @private + */ + _createBaseClipPathSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var reviver = options.reviver, + additionalTransform = options.additionalTransform || '', + commonPieces = [ + this.getSvgTransform(true, additionalTransform), + this.getSvgCommons(), + ].join(''), + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'); + objectMarkup[index] = commonPieces; + return reviver ? reviver(objectMarkup.join('')) : objectMarkup.join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var noStyle = options.noStyle, + reviver = options.reviver, + styleInfo = noStyle ? '' : 'style="' + this.getSvgStyles() + '" ', + shadowInfo = options.withShadow ? 'style="' + this.getSvgFilter() + '" ' : '', + clipPath = this.clipPath, + vectorEffect = this.strokeUniform ? 'vector-effect="non-scaling-stroke" ' : '', + absoluteClipPath = clipPath && clipPath.absolutePositioned, + stroke = this.stroke, fill = this.fill, shadow = this.shadow, + commonPieces, markup = [], clipPathMarkup, + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'), + additionalTransform = options.additionalTransform; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + clipPathMarkup = '\n' + + clipPath.toClipPathSVG(reviver) + + '\n'; + } + if (absoluteClipPath) { + markup.push( + '\n' + ); + } + markup.push( + '\n' + ); + commonPieces = [ + styleInfo, + vectorEffect, + noStyle ? '' : this.addPaintOrder(), ' ', + additionalTransform ? 'transform="' + additionalTransform + '" ' : '', + ].join(''); + objectMarkup[index] = commonPieces; + if (fill && fill.toLive) { + markup.push(fill.toSVG(this)); + } + if (stroke && stroke.toLive) { + markup.push(stroke.toSVG(this)); + } + if (shadow) { + markup.push(shadow.toSVG(this)); + } + if (clipPath) { + markup.push(clipPathMarkup); + } + markup.push(objectMarkup.join('')); + markup.push('\n'); + absoluteClipPath && markup.push('\n'); + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + addPaintOrder: function() { + return this.paintFirst !== 'fill' ? ' paint-order="' + this.paintFirst + '" ' : ''; + } + }); +})(); +/* _TO_SVG_END_ */ + + +(function() { + + var extend = fabric.util.object.extend, + originalSet = 'stateProperties'; + + /* + Depends on `stateProperties` + */ + function saveProps(origin, destination, props) { + var tmpObj = { }, deep = true; + props.forEach(function(prop) { + tmpObj[prop] = origin[prop]; + }); + + extend(origin[destination], tmpObj, deep); + } + + function _isEqual(origValue, currentValue, firstPass) { + if (origValue === currentValue) { + // if the objects are identical, return + return true; + } + else if (Array.isArray(origValue)) { + if (!Array.isArray(currentValue) || origValue.length !== currentValue.length) { + return false; + } + for (var i = 0, len = origValue.length; i < len; i++) { + if (!_isEqual(origValue[i], currentValue[i])) { + return false; + } + } + return true; + } + else if (origValue && typeof origValue === 'object') { + var keys = Object.keys(origValue), key; + if (!currentValue || + typeof currentValue !== 'object' || + (!firstPass && keys.length !== Object.keys(currentValue).length) + ) { + return false; + } + for (var i = 0, len = keys.length; i < len; i++) { + key = keys[i]; + // since clipPath is in the statefull cache list and the clipPath objects + // would be iterated as an object, this would lead to possible infinite recursion + // we do not want to compare those. + if (key === 'canvas' || key === 'group') { + continue; + } + if (!_isEqual(origValue[key], currentValue[key])) { + return false; + } + } + return true; + } + } + + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns true if object state (one of its state properties) was changed + * @param {String} [propertySet] optional name for the set of property we want to save + * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called + */ + hasStateChanged: function(propertySet) { + propertySet = propertySet || originalSet; + var dashedPropertySet = '_' + propertySet; + if (Object.keys(this[dashedPropertySet]).length < this[propertySet].length) { + return true; + } + return !_isEqual(this[dashedPropertySet], this, true); + }, + + /** + * Saves state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + saveState: function(options) { + var propertySet = options && options.propertySet || originalSet, + destination = '_' + propertySet; + if (!this[destination]) { + return this.setupState(options); + } + saveProps(this, destination, this[propertySet]); + if (options && options.stateProperties) { + saveProps(this, destination, options.stateProperties); + } + return this; + }, + + /** + * Setups state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + setupState: function(options) { + options = options || { }; + var propertySet = options.propertySet || originalSet; + options.propertySet = propertySet; + this['_' + propertySet] = { }; + this.saveState(options); + return this; + } + }); +})(); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Determines which corner has been clicked + * @private + * @param {Object} pointer The pointer indicating the mouse position + * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found + */ + _findTargetCorner: function(pointer, forTouch) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { + return false; + } + + var ex = pointer.x, + ey = pointer.y, + xPoints, + lines, keys = Object.keys(this.oCoords), + j = keys.length - 1, i; + this.__corner = 0; + + // cycle in reverse order so we pick first the one on top + for (; j >= 0; j--) { + i = keys[j]; + if (!this.isControlVisible(i)) { + continue; + } + + lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); + // // debugging + // + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); + if (xPoints !== 0 && xPoints % 2 === 1) { + this.__corner = i; + return i; + } + } + return false; + }, + + /** + * Calls a function for each control. The function gets called, + * with the control, the object that is calling the iterator and the control's key + * @param {Function} fn function to iterate over the controls over + */ + forEachControl: function(fn) { + for (var i in this.controls) { + fn(this.controls[i], i, this); + }; + }, + + /** + * Sets the coordinates of the draggable boxes in the corners of + * the image used to scale/rotate it. + * note: if we would switch to ROUND corner area, all of this would disappear. + * everything would resolve to a single point and a pythagorean theorem for the distance + * @private + */ + _setCornerCoords: function() { + var coords = this.oCoords; + + for (var control in coords) { + var controlObject = this.controls[control]; + coords[control].corner = controlObject.calcCornerCoords( + this.angle, this.cornerSize, coords[control].x, coords[control].y, false); + coords[control].touchCorner = controlObject.calcCornerCoords( + this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true); + } + }, + + /** + * Draws a colored layer behind the object, inside its selection borders. + * Requires public options: padding, selectionBackgroundColor + * this function is called when the context is transformed + * has checks to be skipped when the object is on a staticCanvas + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawSelectionBackground: function(ctx) { + if (!this.selectionBackgroundColor || + (this.canvas && !this.canvas.interactive) || + (this.canvas && this.canvas._activeObject !== this) + ) { + return this; + } + ctx.save(); + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + vpt = this.canvas.viewportTransform; + ctx.translate(center.x, center.y); + ctx.scale(1 / vpt[0], 1 / vpt[3]); + ctx.rotate(degreesToRadians(this.angle)); + ctx.fillStyle = this.selectionBackgroundColor; + ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBorders: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + var wh = this._calculateCurrentDimensions(), + strokeWidth = this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls, + shouldStroke = false; + + ctx.save(); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + if (hasControls) { + ctx.beginPath(); + this.forEachControl(function(control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + if (shouldStroke) { + ctx.stroke(); + } + } + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box when it is inside a group. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {object} options object representing current object parameters + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBordersInGroup: function(ctx, options, styleOverride) { + styleOverride = styleOverride || {}; + var bbox = fabric.util.sizeAfterTransform(this.width, this.height, options), + strokeWidth = this.strokeWidth, + strokeUniform = this.strokeUniform, + borderScaleFactor = this.borderScaleFactor, + width = + bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, + height = + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; + ctx.save(); + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + ctx.restore(); + return this; + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height + * Requires public options: cornerSize, padding + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawControls: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + ctx.save(); + var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; + ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); + ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; + if (!this.transparentCorners) { + ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; + } + this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); + this.setCoords(); + if (this.group) { + // fabricJS does not really support drawing controls inside groups, + // this piece of code here helps having at least the control in places. + // If an application needs to show some objects as selected because of some UI state + // can still call Object._renderControls() on any object they desire, independently of groups. + // using no padding, circular controls and hiding the rotating cursor is higly suggested, + matrix = this.group.calcTransformMatrix(); + } + this.forEachControl(function(control, key, fabricObject) { + p = fabricObject.oCoords[key]; + if (control.getVisibility(fabricObject, key)) { + if (matrix) { + p = fabric.util.transformPoint(p, matrix); + } + control.render(ctx, p.x, p.y, styleOverride, fabricObject); + } + }); + ctx.restore(); + + return this; + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlKey) { + return this.controls[controlKey] && this.controls[controlKey].getVisibility(this, controlKey); + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlKey The key of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlKey, visible) { + if (!this._controlsVisibility) { + this._controlsVisibility = {}; + } + this._controlsVisibility[controlKey] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to deselect this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onDeselect: function() { + // implemented by sub-classes, as needed. + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to select this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onSelect: function() { + // implemented by sub-classes, as needed. + } + }); +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Animation duration (in ms) for fx* methods + * @type Number + * @default + */ + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectH: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.left, + endValue: this.getCenter().left, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('left', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Centers object vertically with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectV: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.top, + endValue: this.getCenter().top, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('top', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @param {fabric.Object} object Object to remove + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.opacity, + endValue: 0, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('opacity', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Animates object's properties + * @param {String|Object} property Property to animate (if string) or properties to animate (if object) + * @param {Number|Object} value Value to animate property to (if string was given first) or options object + * @return {fabric.Object} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation} + * @chainable + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + var propsToAnimate = [], prop, skipCallbacks; + for (prop in arguments[0]) { + propsToAnimate.push(prop); + } + for (var i = 0, len = propsToAnimate.length; i < len; i++) { + prop = propsToAnimate[i]; + skipCallbacks = i !== len - 1; + this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @param {String} property Property to animate + * @param {String} to Value to animate to + * @param {Object} [options] Options object + * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked + */ + _animate: function(property, to, options, skipCallbacks) { + var _this = this, propPair; + + to = to.toString(); + + if (!options) { + options = { }; + } + else { + options = fabric.util.object.clone(options); + } + + if (~property.indexOf('.')) { + propPair = property.split('.'); + } + + var propIsColor = + _this.colorProperties.indexOf(property) > -1 || + (propPair && _this.colorProperties.indexOf(propPair[1]) > -1); + + var currentValue = propPair + ? this.get(propPair[0])[propPair[1]] + : this.get(property); + + if (!('from' in options)) { + options.from = currentValue; + } + + if (!propIsColor) { + if (~to.indexOf('=')) { + to = currentValue + parseFloat(to.replace('=', '')); + } + else { + to = parseFloat(to); + } + } + + var _options = { + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + abort: options.abort && function(value, valueProgress, timeProgress) { + return options.abort.call(_this, value, valueProgress, timeProgress); + }, + onChange: function (value, valueProgress, timeProgress) { + if (propPair) { + _this[propPair[0]][propPair[1]] = value; + } + else { + _this.set(property, value); + } + if (skipCallbacks) { + return; + } + options.onChange && options.onChange(value, valueProgress, timeProgress); + }, + onComplete: function (value, valueProgress, timeProgress) { + if (skipCallbacks) { + return; + } + + _this.setCoords(); + options.onComplete && options.onComplete(value, valueProgress, timeProgress); + } + }; + + if (propIsColor) { + return fabric.util.animateColor(_options.startValue, _options.endValue, _options.duration, _options); + } + else { + return fabric.util.animate(_options); + } + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }; + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * Line class + * @class fabric.Line + * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'line', + + /** + * x value or first line edge + * @type Number + * @default + */ + x1: 0, + + /** + * y value or first line edge + * @type Number + * @default + */ + y1: 0, + + /** + * x value or second line edge + * @type Number + * @default + */ + x2: 0, + + /** + * y value or second line edge + * @type Number + * @default + */ + y2: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('x1', 'x2', 'y1', 'y2'), + + /** + * Constructor + * @param {Array} [points] Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} thisArg + */ + initialize: function(points, options) { + if (!points) { + points = [0, 0, 0, 0]; + } + + this.callSuper('initialize', options); + + this.set('x1', points[0]); + this.set('y1', points[1]); + this.set('x2', points[2]); + this.set('y2', points[3]); + + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Object} [options] Options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.width = Math.abs(this.x2 - this.x1); + this.height = Math.abs(this.y2 - this.y1); + + this.left = 'left' in options + ? options.left + : this._getLeftToOriginX(); + + this.top = 'top' in options + ? options.top + : this._getTopToOriginY(); + }, + + /** + * @private + * @param {String} key + * @param {*} value + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + if (typeof coordProps[key] !== 'undefined') { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. + */ + _getLeftToOriginX: makeEdgeToOriginGetter( + { // property names + origin: 'originX', + axis1: 'x1', + axis2: 'x2', + dimension: 'width' + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right' + } + ), + + /** + * @private + * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. + */ + _getTopToOriginY: makeEdgeToOriginGetter( + { // property names + origin: 'originY', + axis1: 'y1', + axis2: 'y2', + dimension: 'height' + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom' + } + ), + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + + var p = this.calcLinePoints(); + ctx.moveTo(p.x1, p.y1); + ctx.lineTo(p.x2, p.y2); + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = this.stroke || ctx.fillStyle; + this.stroke && this._renderStroke(ctx); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); + }, + + /* + * Calculate object dimensions from its properties + * @private + */ + _getNonTransformedDimensions: function() { + var dim = this.callSuper('_getNonTransformedDimensions'); + if (this.strokeLineCap === 'butt') { + if (this.width === 0) { + dim.y -= this.strokeWidth; + } + if (this.height === 0) { + dim.x -= this.strokeWidth; + } + } + return dim; + }, + + /** + * Recalculates line points given width and height + * @private + */ + calcLinePoints: function() { + var xMult = this.x1 <= this.x2 ? -1 : 1, + yMult = this.y1 <= this.y2 ? -1 : 1, + x1 = (xMult * this.width * 0.5), + y1 = (yMult * this.height * 0.5), + x2 = (xMult * this.width * -0.5), + y2 = (yMult * this.height * -0.5); + + return { + x1: x1, + x2: x2, + y1: y1, + y2: y2 + }; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var p = this.calcLinePoints(); + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) + * @static + * @memberOf fabric.Line + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @memberOf fabric.Line + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} [callback] callback function invoked after parsing + */ + fabric.Line.fromElement = function(element, callback, options) { + options = options || { }; + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), + points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + callback(new fabric.Line(points, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Line instance from an object representation + * @static + * @memberOf fabric.Line + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Line.fromObject = function(object, callback) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; + var options = clone(object, true); + options.points = [object.x1, object.y1, object.x2, object.y2]; + fabric.Object._fromObject('Line', options, _callback, 'points'); + }; + + /** + * Produces a function that calculates distance from canvas edge to Line origin. + */ + function makeEdgeToOriginGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + axis1 = propertyNames.axis1, + axis2 = propertyNames.axis2, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + switch (this.get(origin)) { + case nearest: + return Math.min(this.get(axis1), this.get(axis2)); + case center: + return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); + case farthest: + return Math.max(this.get(axis1), this.get(axis2)); + } + }; + + } + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + pi = Math.PI; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * Circle class + * @class fabric.Circle + * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'circle', + + /** + * Radius of this circle + * @type Number + * @default + */ + radius: 0, + + /** + * Start angle of the circle, moving clockwise + * deprecated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 0 + */ + startAngle: 0, + + /** + * End angle of the circle + * deprecated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 2Pi + */ + endAngle: pi * 2, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('radius', 'startAngle', 'endAngle'), + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Circle} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (key === 'radius') { + this.setRadius(value); + } + + return this; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['radius', 'startAngle', 'endAngle'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString, x = 0, y = 0, + angle = (this.endAngle - this.startAngle) % ( 2 * pi); + + if (angle === 0) { + svgString = [ + '\n' + ]; + } + else { + var startX = fabric.util.cos(this.startAngle) * this.radius, + startY = fabric.util.sin(this.startAngle) * this.radius, + endX = fabric.util.cos(this.endAngle) * this.radius, + endY = fabric.util.sin(this.endAngle) * this.radius, + largeFlag = angle > pi ? '1' : '0'; + svgString = [ + '\n' + ]; + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.arc( + 0, + 0, + this.radius, + this.startAngle, + this.endAngle, false); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @return {fabric.Circle} thisArg + */ + setRadius: function(value) { + this.radius = value; + return this.set('width', value * 2).set('height', value * 2); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @memberOf fabric.Circle + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @memberOf fabric.Circle + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @param {Object} [options] Options object + * @throws {Error} If value of `r` attribute is missing or invalid + */ + fabric.Circle.fromElement = function(element, callback) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + + if (!isValidRadius(parsedAttributes)) { + throw new Error('value of `r` attribute is required and can not be negative'); + } + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.radius; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.radius; + callback(new fabric.Circle(parsedAttributes)); + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius >= 0)); + } + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @memberOf fabric.Circle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {void} + */ + fabric.Circle.fromObject = function(object, callback) { + fabric.Object._fromObject('Circle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * Triangle class + * @class fabric.Triangle + * @extends fabric.Object + * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'triangle', + + /** + * Width is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + width: 100, + + /** + * Height is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + height: 100, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + ctx.moveTo(-widthBy2, heightBy2); + ctx.lineTo(0, -heightBy2); + ctx.lineTo(widthBy2, heightBy2); + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2, + points = [ + -widthBy2 + ' ' + heightBy2, + '0 ' + -heightBy2, + widthBy2 + ' ' + heightBy2 + ].join(','); + return [ + '' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Triangle} instance from an object representation + * @static + * @memberOf fabric.Triangle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * Ellipse class + * @class fabric.Ellipse + * @extends fabric.Object + * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'ellipse', + + /** + * Horizontal radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Ellipse} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this.set('rx', options && options.rx || 0); + this.set('ry', options && options.ry || 0); + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Ellipse} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + switch (key) { + + case 'rx': + this.rx = value; + this.set('width', value * 2); + break; + + case 'ry': + this.ry = value; + this.set('height', value * 2); + break; + + } + return this; + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRx: function() { + return this.get('rx') * this.get('scaleX'); + }, + + /** + * Returns Vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRy: function() { + return this.get('ry') * this.get('scaleY'); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.save(); + ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); + ctx.arc( + 0, + 0, + this.rx, + 0, + piBy2, + false); + ctx.restore(); + this._renderPaintInOrder(ctx); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @memberOf fabric.Ellipse + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @memberOf fabric.Ellipse + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, callback) { + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.rx; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.ry; + callback(new fabric.Ellipse(parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Ellipse} instance from an object representation + * @static + * @memberOf fabric.Ellipse + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {void} + */ + fabric.Ellipse.fromObject = function(object, callback) { + fabric.Object._fromObject('Ellipse', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Rect) { + fabric.warn('fabric.Rect is already defined'); + return; + } + + /** + * Rectangle class + * @class fabric.Rect + * @extends fabric.Object + * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { + + /** + * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('rx', 'ry'), + + /** + * Type of an object + * @type String + * @default + */ + type: 'rect', + + /** + * Horizontal border radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical border radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Initializes rx/ry attributes + * @private + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + + // 1x1 case (used in spray brush) optimization was removed because + // with caching and higher zoom level this makes more damage than help + + var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, + ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, + w = this.width, + h = this.height, + x = -this.width / 2, + y = -this.height / 2, + isRounded = rx !== 0 || ry !== 0, + /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ + k = 1 - 0.5522847498; + ctx.beginPath(); + + ctx.moveTo(x + rx, y); + + ctx.lineTo(x + w - rx, y); + isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); + + ctx.lineTo(x + w, y + h - ry); + isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); + + ctx.lineTo(x + rx, y + h); + isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); + + ctx.lineTo(x, y + ry); + isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); + + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var x = -this.width / 2, y = -this.height / 2; + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + * @memberOf fabric.Rect + * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement + */ + fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); + + /** + * Returns {@link fabric.Rect} instance from an SVG element + * @static + * @memberOf fabric.Rect + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Rect.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + options = options || { }; + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + parsedAttributes.height = parsedAttributes.height || 0; + parsedAttributes.width = parsedAttributes.width || 0; + var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect.visible = rect.visible && rect.width > 0 && rect.height > 0; + callback(rect); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Rect} instance from an object representation + * @static + * @memberOf fabric.Rect + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + */ + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * Polyline class + * @class fabric.Polyline + * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polyline', + + /** + * Points array + * @type Array + * @default + */ + points: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), + + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); + */ + initialize: function(points, options) { + options = options || {}; + this.points = points || []; + this.callSuper('initialize', options); + this._setPositionDimensions(options); + }, + + _setPositionDimensions: function(options) { + var calcDim = this._calcDimensions(options), correctLeftTop; + this.width = calcDim.width; + this.height = calcDim.height; + if (!options.fromSVG) { + correctLeftTop = this.translateToGivenOrigin( + { x: calcDim.left - this.strokeWidth / 2, y: calcDim.top - this.strokeWidth / 2 }, + 'left', + 'top', + this.originX, + this.originY + ); + } + if (typeof options.left === 'undefined') { + this.left = options.fromSVG ? calcDim.left : correctLeftTop.x; + } + if (typeof options.top === 'undefined') { + this.top = options.fromSVG ? calcDim.top : correctLeftTop.y; + } + this.pathOffset = { + x: calcDim.left + this.width / 2, + y: calcDim.top + this.height / 2 + }; + }, + + /** + * Calculate the polygon min and max point from points array, + * returning an object with left, top, width, height to measure the + * polygon size + * @return {Object} object.left X coordinate of the polygon leftmost point + * @return {Object} object.top Y coordinate of the polygon topmost point + * @return {Object} object.width distance between X coordinates of the polygon leftmost and rightmost point + * @return {Object} object.height distance between Y coordinates of the polygon topmost and bottommost point + * @private + */ + _calcDimensions: function() { + + var points = this.points, + minX = min(points, 'x') || 0, + minY = min(points, 'y') || 0, + maxX = max(points, 'x') || 0, + maxY = max(points, 'y') || 0, + width = (maxX - minX), + height = (maxY - minY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + points: this.points.concat() + }); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var points = [], diffX = this.pathOffset.x, diffY = this.pathOffset.y, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push( + toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', + toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' + ); + } + return [ + '<' + this.type + ' ', 'COMMON_PARTS', + 'points="', points.join(''), + '" />\n' + ]; + }, + /* _TO_SVG_END_ */ + + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + commonRender: function(ctx) { + var point, len = this.points.length, + x = this.pathOffset.x, + y = this.pathOffset.y; + + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return false; + } + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (var i = 0; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + return true; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + this._renderPaintInOrder(ctx); + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.get('points').length; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) + * @static + * @memberOf fabric.Polyline + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @memberOf fabric.Polyline + * @param {SVGElement} element Element to parser + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polyline.fromElementGenerator = function(_class) { + return function(element, callback, options) { + if (!element) { + return callback(null); + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric[_class].ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric[_class](points, extend(parsedAttributes, options))); + }; + }; + + fabric.Polyline.fromElement = fabric.Polyline.fromElementGenerator('Polyline'); + + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @memberOf fabric.Polyline + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * Polygon class + * @class fabric.Polygon + * @extends fabric.Polyline + * @see {@link fabric.Polygon#initialize} for constructor definition + */ + fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polygon', + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + ctx.closePath(); + this._renderPaintInOrder(ctx); + }, + + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @memberOf fabric.Polygon + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns {@link fabric.Polygon} instance from an SVG element + * @static + * @memberOf fabric.Polygon + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @memberOf fabric.Polygon + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @return {void} + */ + fabric.Polygon.fromObject = function(object, callback) { + fabric.Object._fromObject('Polygon', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max, + extend = fabric.util.object.extend, + _toString = Object.prototype.toString, + toFixed = fabric.util.toFixed; + + if (fabric.Path) { + fabric.warn('fabric.Path is already defined'); + return; + } + + /** + * Path class + * @class fabric.Path + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition + */ + fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path', + + /** + * Array of path points + * @type Array + * @default + */ + path: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('path', 'fillRule'), + + stateProperties: fabric.Object.prototype.stateProperties.concat('path'), + + /** + * Constructor + * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) + * @param {Object} [options] Options object + * @return {fabric.Path} thisArg + */ + initialize: function(path, options) { + options = options || { }; + this.callSuper('initialize', options); + if (!path) { + path = []; + } + + var fromArray = _toString.call(path) === '[object Array]'; + + this.path = fabric.util.makePathSimpler( + fromArray ? path : fabric.util.parsePath(path) + ); + + if (!this.path) { + return; + } + fabric.Polyline.prototype._setPositionDimensions.call(this, options); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _renderPathCommands: function(ctx) { + var current, // current instruction + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + l = -this.pathOffset.x, + t = -this.pathOffset.y; + + ctx.beginPath(); + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'C': // bezierCurveTo, absolute + x = current[5]; + y = current[6]; + controlX = current[3]; + controlY = current[4]; + ctx.bezierCurveTo( + current[1] + l, + current[2] + t, + controlX + l, + controlY + t, + x + l, + y + t + ); + break; + + case 'Q': // quadraticCurveTo, absolute + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + current[3] + l, + current[4] + t + ); + x = current[3]; + y = current[4]; + controlX = current[1]; + controlY = current[2]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + ctx.closePath(); + break; + } + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _render: function(ctx) { + this._renderPathCommands(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns string representation of an instance + * @return {String} string representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), { + path: this.path.map(function(item) { return item.slice(); }), + }); + }, + + /** + * Returns dataless object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var o = this.toObject(['sourcePath'].concat(propertiesToInclude)); + if (o.sourcePath) { + delete o.path; + } + return o; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var path = fabric.util.joinPath(this.path); + return [ + '\n' + ]; + }, + + _getOffsetTransform: function() { + var digits = fabric.Object.NUM_FRACTION_DIGITS; + return ' translate(' + toFixed(-this.pathOffset.x, digits) + ', ' + + toFixed(-this.pathOffset.y, digits) + ')'; + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return '\t' + this._createBaseClipPathSVGMarkup( + this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform } + ); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return this._createBaseSVGMarkup(this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform }); + }, + /* _TO_SVG_END_ */ + + /** + * Returns number representation of an instance complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + */ + _calcDimensions: function() { + + var aX = [], + aY = [], + current, // current instruction + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + bounds; + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + bounds = []; + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'C': // bezierCurveTo, absolute + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + x = current[5]; + y = current[6]; + break; + + case 'Q': // quadraticCurveTo, absolute + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + current[1], + current[2], + current[3], + current[4] + ); + x = current[3]; + y = current[4]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + break; + } + bounds.forEach(function (point) { + aX.push(point.x); + aY.push(point.y); + }); + aX.push(x); + aY.push(y); + } + + var minX = min(aX) || 0, + minY = min(aY) || 0, + maxX = max(aX) || 0, + maxY = max(aY) || 0, + deltaX = maxX - minX, + deltaY = maxY - minY; + + return { + left: minX, + top: minY, + width: deltaX, + height: deltaY + }; + } + }); + + /** + * Creates an instance of fabric.Path from an object + * @static + * @memberOf fabric.Path + * @param {Object} object + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.sourcePath === 'string') { + var pathUrl = object.sourcePath; + fabric.loadSVGFromURL(pathUrl, function (elements) { + var path = elements[0]; + path.setOptions(object); + callback && callback(path); + }); + } + else { + fabric.Object._fromObject('Path', object, callback, 'path'); + } + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @memberOf fabric.Path + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); + + /** + * Creates an instance of fabric.Path from an SVG element + * @static + * @memberOf fabric.Path + * @param {SVGElement} element to parse + * @param {Function} callback Callback to invoke when an fabric.Path instance is created + * @param {Object} [options] Options object + * @param {Function} [callback] Options callback invoked after parsing is finished + */ + fabric.Path.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max; + + if (fabric.Group) { + return; + } + + /** + * Group class + * @class fabric.Group + * @extends fabric.Object + * @mixes fabric.Collection + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.Group#initialize} for constructor definition + */ + fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'group', + + /** + * Width of stroke + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets + * @type Boolean + * @default + */ + subTargetCheck: false, + + /** + * Groups are container, do not render anything on theyr own, ence no cache properties + * @type Array + * @default + */ + cacheProperties: [], + + /** + * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still + * available setting this boolean to true. + * @type Boolean + * @since 2.0.0 + * @default + */ + useSetOnGroup: false, + + /** + * Constructor + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. + * @return {Object} thisArg + */ + initialize: function(objects, options, isAlreadyGrouped) { + options = options || {}; + this._objects = []; + // if objects enclosed in a group have been grouped already, + // we cannot change properties of objects. + // Thus we need to set options to group without objects, + isAlreadyGrouped && this.callSuper('initialize', options); + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (!isAlreadyGrouped) { + var center = options && options.centerPoint; + // we want to set origins before calculating the bounding box. + // so that the topleft can be set with that in mind. + // if specific top and left are passed, are overwritten later + // with the callSuper('initialize', options) + if (options.originX !== undefined) { + this.originX = options.originX; + } + if (options.originY !== undefined) { + this.originY = options.originY; + } + // if coming from svg i do not want to calc bounds. + // i assume width and height are passed along options + center || this._calcBounds(); + this._updateObjectsCoords(center); + delete options.centerPoint; + this.callSuper('initialize', options); + } + else { + this._updateObjectsACoords(); + } + + this.setCoords(); + }, + + /** + * @private + */ + _updateObjectsACoords: function() { + var skipControls = true; + for (var i = this._objects.length; i--; ){ + this._objects[i].setCoords(skipControls); + } + }, + + /** + * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + */ + _updateObjectsCoords: function(center) { + var center = center || this.getCenterPoint(); + for (var i = this._objects.length; i--; ){ + this._updateObjectCoords(this._objects[i], center); + } + }, + + /** + * @private + * @param {Object} object + * @param {fabric.Point} center, current center of group. + */ + _updateObjectCoords: function(object, center) { + var objectLeft = object.left, + objectTop = object.top, + skipControls = true; + + object.set({ + left: objectLeft - center.x, + top: objectTop - center.y + }); + object.group = this; + object.setCoords(skipControls); + }, + + /** + * Returns string represenation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Adds an object to a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + var nested = !!this.group; + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + if (object) { + if (nested) { + // if this group is inside another group, we need to pre transform the object + fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); + } + this._objects.push(object); + object.group = this; + object._set('canvas', this.canvas); + } + this._calcBounds(); + this._updateObjectsCoords(); + this.dirty = true; + if (nested) { + this.group.addWithUpdate(); + } + else { + this.setCoords(); + } + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + this.setCoords(); + this.dirty = true; + return this; + }, + + /** + * @private + */ + _onObjectAdded: function(object) { + this.dirty = true; + object.group = this; + object._set('canvas', this.canvas); + }, + + /** + * @private + */ + _onObjectRemoved: function(object) { + this.dirty = true; + delete object.group; + }, + + /** + * @private + */ + _set: function(key, value) { + var i = this._objects.length; + if (this.useSetOnGroup) { + while (i--) { + this._objects[i].setOnGroup(key, value); + } + } + if (key === 'canvas') { + while (i--) { + this._objects[i]._set(key, value); + } + } + fabric.Object.prototype._set.call(this, key, value); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + var _includeDefaultValues = this.includeDefaultValues; + var objsToObject = this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .map(function (obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Returns object representation of an instance, in dataless mode. + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var objsToObject, sourcePath = this.sourcePath; + if (sourcePath) { + objsToObject = sourcePath; + } + else { + var _includeDefaultValues = this.includeDefaultValues; + objsToObject = this._objects.map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toDatalessObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + } + var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx) { + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group is already cached. + * @return {Boolean} + */ + shouldCache: function() { + var ownCache = fabric.Object.prototype.shouldCache.call(this); + if (ownCache) { + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + this.ownCaching = false; + return false; + } + } + } + return ownCache; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (fabric.Object.prototype.willDrawShadow.call(this)) { + return true; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return this.ownCaching || (this.group && this.group.isOnACache()); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].render(ctx); + } + this._drawClipPath(ctx); + }, + + /** + * Check if cache is dirty + */ + isCacheDirty: function(skipCanvas) { + if (this.callSuper('isCacheDirty', skipCanvas)) { + return true; + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].isCacheDirty(true)) { + if (this._cacheCanvas) { + // if this group has not a cache canvas there is nothing to clean + var x = this.cacheWidth / this.zoomX, y = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-x / 2, -y / 2, x, y); + } + return true; + } + } + return false; + }, + + /** + * Restores original state of each of group objects (original state is that which was before group was created). + * if the nested boolean is true, the original state will be restored just for the + * first group and not for all the group chain + * @private + * @param {Boolean} nested tell the function to restore object state up to the parent group and not more + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + var groupMatrix = this.calcOwnMatrix(); + this._objects.forEach(function(object) { + // instead of using _this = this; + fabric.util.addTransformToObject(object, groupMatrix); + delete object.group; + object.setCoords(); + }); + return this; + }, + + /** + * Realises the transform from this group onto the supplied object + * i.e. it tells you what would happen if the supplied object was in + * the group, and then the group was destroyed. It mutates the supplied + * object. + * Warning: this method is not useful anymore, it has been kept to no break the api. + * is not used in the fabricJS codebase + * this method will be reduced to using the utility. + * @private + * @deprecated + * @param {fabric.Object} object + * @param {Array} parentMatrix parent transformation + * @return {fabric.Object} transformedObject + */ + realizeTransform: function(object, parentMatrix) { + fabric.util.addTransformToObject(object, parentMatrix); + return object; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + // when group is destroyed objects needs to get a repaint to be eventually + // displayed on canvas. + this._objects.forEach(function(object) { + object.set('dirty', true); + }); + return this._restoreObjectsState(); + }, + + /** + * make a group an active selection, remove the group from canvas + * the group has to be on canvas for this to work. + * @return {fabric.ActiveSelection} thisArg + * @chainable + */ + toActiveSelection: function() { + if (!this.canvas) { + return; + } + var objects = this._objects, canvas = this.canvas; + this._objects = []; + var options = this.toObject(); + delete options.objects; + var activeSelection = new fabric.ActiveSelection([]); + activeSelection.set(options); + activeSelection.type = 'activeSelection'; + canvas.remove(this); + objects.forEach(function(object) { + object.group = activeSelection; + object.dirty = true; + canvas.add(object); + }); + activeSelection.canvas = canvas; + activeSelection._objects = objects; + canvas._activeObject = activeSelection; + activeSelection.setCoords(); + return activeSelection; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + ungroupOnCanvas: function() { + return this._restoreObjectsState(); + }, + + /** + * Sets coordinates of all objects inside group + * @return {fabric.Group} thisArg + * @chainable + */ + setObjectsCoords: function() { + var skipControls = true; + this.forEachObject(function(object) { + object.setCoords(skipControls); + }); + return this; + }, + + /** + * @private + */ + _calcBounds: function(onlyWidthHeight) { + var aX = [], + aY = [], + o, prop, coords, + props = ['tr', 'br', 'bl', 'tl'], + i = 0, iLen = this._objects.length, + j, jLen = props.length; + + for ( ; i < iLen; ++i) { + o = this._objects[i]; + coords = o.calcACoords(); + for (j = 0; j < jLen; j++) { + prop = props[j]; + aX.push(coords[prop].x); + aY.push(coords[prop].y); + } + o.aCoords = coords; + } + + this._getBounds(aX, aY, onlyWidthHeight); + }, + + /** + * @private + */ + _getBounds: function(aX, aY, onlyWidthHeight) { + var minXY = new fabric.Point(min(aX), min(aY)), + maxXY = new fabric.Point(max(aX), max(aY)), + top = minXY.y || 0, left = minXY.x || 0, + width = (maxXY.x - minXY.x) || 0, + height = (maxXY.y - minXY.y) || 0; + this.width = width; + this.height = height; + if (!onlyWidthHeight) { + // the bounding box always finds the topleft most corner. + // whatever is the group origin, we set up here the left/top position. + this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); + } + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function(reviver) { + var svgString = ['\n']; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; + }, + + /** + * Returns styles-string for svg-export, specific version for group + * @return {String} + */ + getSvgStyles: function() { + var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? + 'opacity: ' + this.opacity + ';' : '', + visibility = this.visible ? '' : ' visibility: hidden;'; + return [ + opacity, + this.getSvgFilter(), + visibility + ].join(''); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var svgString = []; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); + } + + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Group} instance from an object representation + * @static + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an group instance is created + */ + fabric.Group.fromObject = function(object, callback) { + var objects = object.objects, + options = fabric.util.object.clone(object, true); + delete options.objects; + if (typeof objects === 'string') { + // it has to be an url or something went wrong. + fabric.loadSVGFromURL(objects, function (elements) { + var group = fabric.util.groupSVGElements(elements, object, objects); + group.set(options); + callback && callback(group); + }); + return; + } + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + fabric.util.enlivenObjects([object.clipPath], function(enlivedClipPath) { + var options = fabric.util.object.clone(object, true); + options.clipPath = enlivedClipPath[0]; + delete options.objects; + callback && callback(new fabric.Group(enlivenedObjects, options, true)); + }); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.ActiveSelection) { + return; + } + + /** + * Group class + * @class fabric.ActiveSelection + * @extends fabric.Group + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.ActiveSelection#initialize} for constructor definition + */ + fabric.ActiveSelection = fabric.util.createClass(fabric.Group, /** @lends fabric.ActiveSelection.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'activeSelection', + + /** + * Constructor + * @param {Object} objects ActiveSelection objects + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(objects, options) { + options = options || {}; + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (options.originX) { + this.originX = options.originX; + } + if (options.originY) { + this.originY = options.originY; + } + this._calcBounds(); + this._updateObjectsCoords(); + fabric.Object.prototype.initialize.call(this, options); + this.setCoords(); + }, + + /** + * Change te activeSelection to a normal group, + * High level function that automatically adds it to canvas as + * active object. no events fired. + * @since 2.0.0 + * @return {fabric.Group} + */ + toGroup: function() { + var objects = this._objects.concat(); + this._objects = []; + var options = fabric.Object.prototype.toObject.call(this); + var newGroup = new fabric.Group([]); + delete options.type; + newGroup.set(options); + objects.forEach(function(object) { + object.canvas.remove(object); + object.group = newGroup; + }); + newGroup._objects = objects; + if (!this.canvas) { + return newGroup; + } + var canvas = this.canvas; + canvas.add(newGroup); + canvas._activeObject = newGroup; + newGroup.setCoords(); + return newGroup; + }, + + /** + * If returns true, deselection is cancelled. + * @since 2.0.0 + * @return {Boolean} [cancel] + */ + onDeselect: function() { + this.destroy(); + return false; + }, + + /** + * Returns string representation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return false; + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + * @param {Object} [childrenOverride] properties to override the children overrides + */ + _renderControls: function(ctx, styleOverride, childrenOverride) { + ctx.save(); + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + this.callSuper('_renderControls', ctx, styleOverride); + childrenOverride = childrenOverride || { }; + if (typeof childrenOverride.hasControls === 'undefined') { + childrenOverride.hasControls = false; + } + childrenOverride.forActiveSelection = true; + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx, childrenOverride); + } + ctx.restore(); + }, + }); + + /** + * Returns {@link fabric.ActiveSelection} instance from an object representation + * @static + * @memberOf fabric.ActiveSelection + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + */ + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + /** + * Image class + * @class fabric.Image + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} + * @see {@link fabric.Image#initialize} for constructor definition + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'image', + + /** + * Width of a stroke. + * For image quality a stroke multiple of 2 gives better results. + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * When calling {@link fabric.Image.getSrc}, return value from element src with `element.getAttribute('src')`. + * This allows for relative urls as image src. + * @since 2.7.0 + * @type Boolean + * @default + */ + srcFromAttribute: false, + + /** + * private + * contains last value of scaleX to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleX: 1, + + /** + * private + * contains last value of scaleY to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleY: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingX: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingY: 1, + + /** + * minimum scale factor under which any resizeFilter is triggered to resize the image + * 0 will disable the automatic resize. 1 will trigger automatically always. + * number bigger than 1 are not implemented yet. + * @type Number + */ + minimumScaleTrigger: 0.5, + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat('cropX', 'cropY'), + + /** + * key used to retrieve the texture representing this image + * @since 2.0.0 + * @type String + * @default + */ + cacheKey: '', + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropX: 0, + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropY: 0, + + /** + * Indicates whether this canvas will use image smoothing when painting this image. + * Also influence if the cacheCanvas for this image uses imageSmoothing + * @since 4.0.0-beta.11 + * @type Boolean + * @default + */ + imageSmoothing: true, + + /** + * Constructor + * Image can be initialized with any canvas drawable or a string. + * The string should be a url and will be loaded as an image. + * Canvas and Image element work out of the box, while videos require extra code to work. + * Please check video element events for seeking. + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element + * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. + * @return {fabric.Image} thisArg + */ + initialize: function(element, options) { + options || (options = { }); + this.filters = []; + this.cacheKey = 'texture' + fabric.Object.__uid++; + this.callSuper('initialize', options); + this._initElement(element, options); + }, + + /** + * Returns image element which this instance if based on + * @return {HTMLImageElement} Image element + */ + getElement: function() { + return this._element || {}; + }, + + /** + * Sets image element for this instance to a specified one. + * If filters defined they are applied to new image. + * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * @param {HTMLImageElement} element + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element, options) { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._element = element; + this._originalElement = element; + this._initConfig(options); + if (this.filters.length !== 0) { + this.applyFilters(); + } + // resizeFilters work on the already filtered copy. + // we need to apply resizeFilters AFTER normal filters. + // applyResizeFilters is run more often than normal filters + // and is triggered by user interactions rather than dev code + if (this.resizeFilter) { + this.applyResizeFilters(); + } + return this; + }, + + /** + * Delete a single texture if in webgl mode + */ + removeTexture: function(key) { + var backend = fabric.filterBackend; + if (backend && backend.evictCachesForKey) { + backend.evictCachesForKey(key); + } + }, + + /** + * Delete textures, reference to elements and eventually JSDOM cleanup + */ + dispose: function() { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._cacheContext = undefined; + ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + }, + + /** + * Get the crossOrigin value (of the corresponding image element) + */ + getCrossOrigin: function() { + return this._originalElement && (this._originalElement.crossOrigin || null); + }, + + /** + * Returns original size of an image + * @return {Object} Object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.naturalWidth || element.width, + height: element.naturalHeight || element.height + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + var w = this.width / 2, h = this.height / 2; + ctx.beginPath(); + ctx.moveTo(-w, -h); + ctx.lineTo(w, -h); + ctx.lineTo(w, h); + ctx.lineTo(-w, h); + ctx.lineTo(-w, -h); + ctx.closePath(); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var filters = []; + + this.filters.forEach(function(filterObj) { + if (filterObj) { + filters.push(filterObj.toObject()); + } + }); + var object = extend( + this.callSuper( + 'toObject', + ['cropX', 'cropY'].concat(propertiesToInclude) + ), { + src: this.getSrc(), + crossOrigin: this.getCrossOrigin(), + filters: filters, + }); + if (this.resizeFilter) { + object.resizeFilter = this.resizeFilter.toObject(); + } + return object; + }, + + /** + * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. + * @return {Boolean} + */ + hasCrop: function() { + return this.cropX || this.cropY || this.width < this._element.width || this.height < this._element.height; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString = [], imageMarkup = [], strokeSvg, element = this._element, + x = -this.width / 2, y = -this.height / 2, clipPath = '', imageRendering = ''; + if (!element) { + return []; + } + if (this.hasCrop()) { + var clipPathId = fabric.Object.__uid++; + svgString.push( + '\n', + '\t\n', + '\n' + ); + clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; + } + if (!this.imageSmoothing) { + imageRendering = '" image-rendering="optimizeSpeed'; + } + imageMarkup.push('\t\n'); + + if (this.stroke || this.strokeDashArray) { + var origFill = this.fill; + this.fill = null; + strokeSvg = [ + '\t\n' + ]; + this.fill = origFill; + } + if (this.paintFirst !== 'fill') { + svgString = svgString.concat(strokeSvg, imageMarkup); + } + else { + svgString = svgString.concat(imageMarkup, strokeSvg); + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * Returns source of an image + * @param {Boolean} filtered indicates if the src is needed for svg + * @return {String} Source of an image + */ + getSrc: function(filtered) { + var element = filtered ? this._element : this._originalElement; + if (element) { + if (element.toDataURL) { + return element.toDataURL(); + } + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } + else { + return element.src; + } + } + else { + return this.src || ''; + } + }, + + /** + * Sets source of an image + * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) + * @param {Object} [options] Options object + * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @return {fabric.Image} thisArg + * @chainable + */ + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img, isError) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this, isError); + }, this, options && options.crossOrigin); + return this; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + applyResizeFilters: function() { + var filter = this.resizeFilter, + minimumScale = this.minimumScaleTrigger, + objectScale = this.getTotalObjectScaling(), + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, + elementToFilter = this._filteredEl || this._originalElement; + if (this.group) { + this.set('dirty', true); + } + if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { + this._element = elementToFilter; + this._filterScalingX = 1; + this._filterScalingY = 1; + this._lastScaleX = scaleX; + this._lastScaleY = scaleY; + return; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + var canvasEl = fabric.util.createCanvasElement(), + cacheKey = this._filteredEl ? (this.cacheKey + '_filtered') : this.cacheKey, + sourceWidth = elementToFilter.width, sourceHeight = elementToFilter.height; + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._lastScaleX = filter.scaleX = scaleX; + this._lastScaleY = filter.scaleY = scaleY; + fabric.filterBackend.applyFilters( + [filter], elementToFilter, sourceWidth, sourceHeight, this._element, cacheKey); + this._filterScalingX = canvasEl.width / this._originalElement.width; + this._filterScalingY = canvasEl.height / this._originalElement.height; + }, + + /** + * Applies filters assigned to this image (from "filters" array) or from filter param + * @method applyFilters + * @param {Array} filters to be applied + * @param {Boolean} forResizing specify if the filter operation is a resize operation + * @return {thisArg} return the fabric.Image object + * @chainable + */ + applyFilters: function(filters) { + + filters = filters || this.filters || []; + filters = filters.filter(function(filter) { return filter && !filter.isNeutralState(); }); + this.set('dirty', true); + + // needs to clear out or WEBGL will not resize correctly + this.removeTexture(this.cacheKey + '_filtered'); + + if (filters.length === 0) { + this._element = this._originalElement; + this._filteredEl = null; + this._filterScalingX = 1; + this._filterScalingY = 1; + return this; + } + + var imgElement = this._originalElement, + sourceWidth = imgElement.naturalWidth || imgElement.width, + sourceHeight = imgElement.naturalHeight || imgElement.height; + + if (this._element === this._originalElement) { + // if the element is the same we need to create a new element + var canvasEl = fabric.util.createCanvasElement(); + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._filteredEl = canvasEl; + } + else { + // clear the existing element to get new filter data + // also dereference the eventual resized _element + this._element = this._filteredEl; + this._filteredEl.getContext('2d').clearRect(0, 0, sourceWidth, sourceHeight); + // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y + this._lastScaleX = 1; + this._lastScaleY = 1; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + fabric.filterBackend.applyFilters( + filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey); + if (this._originalElement.width !== this._element.width || + this._originalElement.height !== this._element.height) { + this._filterScalingX = this._element.width / this._originalElement.width; + this._filterScalingY = this._element.height / this._originalElement.height; + } + return this; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { + this.applyResizeFilters(); + } + this._stroke(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * it will set the imageSmoothing for the draw operation + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + fabric.Object.prototype.drawCacheOnCanvas.call(this, ctx); + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * This is the special image version where we would like to avoid caching where possible. + * Essentially images do not benefit from caching. They may require caching, and in that + * case we do it. Also caching an image usually ends in a loss of details. + * A full performance audit should be done. + * @return {Boolean} + */ + shouldCache: function() { + return this.needsItsOwnCache(); + }, + + _renderFill: function(ctx) { + var elementToDraw = this._element; + if (!elementToDraw) { + return; + } + var scaleX = this._filterScalingX, scaleY = this._filterScalingY, + w = this.width, h = this.height, min = Math.min, max = Math.max, + // crop values cannot be lesser than 0. + cropX = max(this.cropX, 0), cropY = max(this.cropY, 0), + elWidth = elementToDraw.naturalWidth || elementToDraw.width, + elHeight = elementToDraw.naturalHeight || elementToDraw.height, + sX = cropX * scaleX, + sY = cropY * scaleY, + // the width height cannot exceed element width/height, starting from the crop offset. + sW = min(w * scaleX, elWidth - sX), + sH = min(h * scaleY, elHeight - sY), + x = -w / 2, y = -h / 2, + maxDestW = min(w, elWidth / scaleX - cropX), + maxDestH = min(h, elHeight / scaleY - cropY); + + elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH); + }, + + /** + * needed to check if image needs resize + * @private + */ + _needsResize: function() { + var scale = this.getTotalObjectScaling(); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); + }, + + /** + * @private + */ + _resetWidthHeight: function() { + this.set(this.getOriginalSize()); + }, + + /** + * The Image class's initialization method. This method is automatically + * called by the constructor. + * @private + * @param {HTMLImageElement|String} element The element representing the image + * @param {Object} [options] Options object + */ + _initElement: function(element, options) { + this.setElement(fabric.util.getById(element), options); + fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initConfig: function(options) { + options || (options = { }); + this.setOptions(options); + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + + /** + * @private + * Set the width and the height of the image object, using the element or the + * options. + * @param {Object} [options] Object with width/height properties + */ + _setWidthHeight: function(options) { + options || (options = { }); + var el = this.getElement(); + this.width = options.width || el.naturalWidth || el.width || 0; + this.height = options.height || el.naturalHeight || el.height || 0; + }, + + /** + * Calculate offset for center and scale factor for the image in order to respect + * the preserveAspectRatio attribute + * @private + * @return {Object} + */ + parsePreserveAspectRatioAttribute: function() { + var pAR = fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio || ''), + rWidth = this._element.width, rHeight = this._element.height, + scaleX = 1, scaleY = 1, offsetLeft = 0, offsetTop = 0, cropX = 0, cropY = 0, + offset, pWidth = this.width, pHeight = this.height, parsedAttributes = { width: pWidth, height: pHeight }; + if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { + if (pAR.meetOrSlice === 'meet') { + scaleX = scaleY = fabric.util.findScaleToFit(this._element, parsedAttributes); + offset = (pWidth - rWidth * scaleX) / 2; + if (pAR.alignX === 'Min') { + offsetLeft = -offset; + } + if (pAR.alignX === 'Max') { + offsetLeft = offset; + } + offset = (pHeight - rHeight * scaleY) / 2; + if (pAR.alignY === 'Min') { + offsetTop = -offset; + } + if (pAR.alignY === 'Max') { + offsetTop = offset; + } + } + if (pAR.meetOrSlice === 'slice') { + scaleX = scaleY = fabric.util.findScaleToCover(this._element, parsedAttributes); + offset = rWidth - pWidth / scaleX; + if (pAR.alignX === 'Mid') { + cropX = offset / 2; + } + if (pAR.alignX === 'Max') { + cropX = offset; + } + offset = rHeight - pHeight / scaleY; + if (pAR.alignY === 'Mid') { + cropY = offset / 2; + } + if (pAR.alignY === 'Max') { + cropY = offset; + } + rWidth = pWidth / scaleX; + rHeight = pHeight / scaleY; + } + } + else { + scaleX = pWidth / rWidth; + scaleY = pHeight / rHeight; + } + return { + width: rWidth, + height: rHeight, + scaleX: scaleX, + scaleY: scaleY, + offsetLeft: offsetLeft, + offsetTop: offsetTop, + cropX: cropX, + cropY: cropY + }; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + * @default + */ + fabric.Image.CSS_CANVAS = 'canvas-img'; + + /** + * Alias for getSrc + * @static + */ + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} callback Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, isError) { + if (isError) { + callback && callback(null, true); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var image = new fabric.Image(img, object); + callback(image, false); + }); + }); + }); + }, null, object.crossOrigin); + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + fabric.util.loadImage(url, function(img, isError) { + callback && callback(new fabric.Image(img, imgOptions), isError); + }, null, imgOptions && imgOptions.crossOrigin); + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} + */ + fabric.Image.ATTRIBUTE_NAMES = + fabric.SHARED_ATTRIBUTES.concat( + 'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split(' ') + ); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} callback Callback to execute when fabric.Image object is created + * @return {fabric.Image} Instance of fabric.Image + */ + fabric.Image.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * @private + * @return {Number} angle value + */ + _getAngleValueForStraighten: function() { + var angle = this.angle % 360; + if (angle > 0) { + return Math.round((angle - 1) / 90) * 90; + } + return Math.round(angle / 90) * 90; + }, + + /** + * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) + * @return {fabric.Object} thisArg + * @chainable + */ + straighten: function() { + this.rotate(this._getAngleValueForStraighten()); + return this; + }, + + /** + * Same as {@link fabric.Object.prototype.straighten} but with animation + * @param {Object} callbacks Object with callback functions + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Object} thisArg + * @chainable + */ + fxStraighten: function(callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: this.get('angle'), + endValue: this._getAngleValueForStraighten(), + duration: this.FX_DURATION, + onChange: function(value) { + _this.rotate(value); + onChange(); + }, + onComplete: function() { + _this.setCoords(); + onComplete(); + }, + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Straightens object, then rerenders canvas + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.requestRenderAll(); + return this; + }, + + /** + * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.requestRenderAllBound + }); + return this; + } +}); + + +(function() { + + 'use strict'; + + /** + * Tests if webgl supports certain precision + * @param {WebGL} Canvas WebGL context to test on + * @param {String} Precision to test can be any of following: 'lowp', 'mediump', 'highp' + * @returns {Boolean} Whether the user's browser WebGL supports given precision. + */ + function testPrecision(gl, precision){ + var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}'; + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + return false; + } + return true; + } + + /** + * Indicate whether this filtering backend is supported by the user's browser. + * @param {Number} tileSize check if the tileSize is supported + * @returns {Boolean} Whether the user's browser supports WebGL. + */ + fabric.isWebglSupported = function(tileSize) { + if (fabric.isLikelyNode) { + return false; + } + tileSize = tileSize || fabric.WebglFilterBackend.prototype.tileSize; + var canvas = document.createElement('canvas'); + var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + var isSupported = false; + // eslint-disable-next-line + if (gl) { + fabric.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + isSupported = fabric.maxTextureSize >= tileSize; + var precisions = ['highp', 'mediump', 'lowp']; + for (var i = 0; i < 3; i++){ + if (testPrecision(gl, precisions[i])){ + fabric.webGlPrecision = precisions[i]; + break; + }; + } + } + this.isSupported = isSupported; + return isSupported; + }; + + fabric.WebglFilterBackend = WebglFilterBackend; + + /** + * WebGL filter backend. + */ + function WebglFilterBackend(options) { + if (options && options.tileSize) { + this.tileSize = options.tileSize; + } + this.setupGLContext(this.tileSize, this.tileSize); + this.captureGPUInfo(); + }; + + WebglFilterBackend.prototype = /** @lends fabric.WebglFilterBackend.prototype */ { + + tileSize: 2048, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Setup a WebGL context suitable for filtering, and bind any needed event handlers. + */ + setupGLContext: function(width, height) { + this.dispose(); + this.createWebGLCanvas(width, height); + // eslint-disable-next-line + this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); + this.chooseFastestCopyGLTo2DMethod(width, height); + }, + + /** + * Pick a method to copy data from GL context to 2d canvas. In some browsers using + * putImageData is faster than drawImage for that specific operation. + */ + chooseFastestCopyGLTo2DMethod: function(width, height) { + var canMeasurePerf = typeof window.performance !== 'undefined', canUseImageData; + try { + new ImageData(1, 1); + canUseImageData = true; + } + catch (e) { + canUseImageData = false; + } + // eslint-disable-next-line no-undef + var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; + // eslint-disable-next-line no-undef + var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; + + if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) { + return; + } + + var targetCanvas = fabric.util.createCanvasElement(); + // eslint-disable-next-line no-undef + var imageBuffer = new ArrayBuffer(width * height * 4); + if (fabric.forceGLPutImageData) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + return; + } + var testContext = { + imageBuffer: imageBuffer, + destinationWidth: width, + destinationHeight: height, + targetCanvas: targetCanvas + }; + var startTime, drawImageTime, putImageDataTime; + targetCanvas.width = width; + targetCanvas.height = height; + + startTime = window.performance.now(); + copyGLTo2DDrawImage.call(testContext, this.gl, testContext); + drawImageTime = window.performance.now() - startTime; + + startTime = window.performance.now(); + copyGLTo2DPutImageData.call(testContext, this.gl, testContext); + putImageDataTime = window.performance.now() - startTime; + + if (drawImageTime > putImageDataTime) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + } + else { + this.copyGLTo2D = copyGLTo2DDrawImage; + } + }, + + /** + * Create a canvas element and associated WebGL context and attaches them as + * class properties to the GLFilterBackend class. + */ + createWebGLCanvas: function(width, height) { + var canvas = fabric.util.createCanvasElement(); + canvas.width = width; + canvas.height = height; + var glOptions = { + alpha: true, + premultipliedAlpha: false, + depth: false, + stencil: false, + antialias: false + }, + gl = canvas.getContext('webgl', glOptions); + if (!gl) { + gl = canvas.getContext('experimental-webgl', glOptions); + } + if (!gl) { + return; + } + gl.clearColor(0, 0, 0, 0); + // this canvas can fire webglcontextlost and webglcontextrestored + this.canvas = canvas; + this.gl = gl; + }, + + /** + * Attempts to apply the requested filters to the source provided, drawing the filtered output + * to the provided target canvas. + * + * @param {Array} filters The filters to apply. + * @param {HTMLImageElement|HTMLCanvasElement} source The source to be filtered. + * @param {Number} width The width of the source input. + * @param {Number} height The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + * @param {String|undefined} cacheKey A key used to cache resources related to the source. If + * omitted, caching will be skipped. + */ + applyFilters: function(filters, source, width, height, targetCanvas, cacheKey) { + var gl = this.gl; + var cachedTexture; + if (cacheKey) { + cachedTexture = this.getCachedTexture(cacheKey, source); + } + var pipelineState = { + originalWidth: source.width || source.originalWidth, + originalHeight: source.height || source.originalHeight, + sourceWidth: width, + sourceHeight: height, + destinationWidth: width, + destinationHeight: height, + context: gl, + sourceTexture: this.createTexture(gl, width, height, !cachedTexture && source), + targetTexture: this.createTexture(gl, width, height), + originalTexture: cachedTexture || + this.createTexture(gl, width, height, !cachedTexture && source), + passes: filters.length, + webgl: true, + aPosition: this.aPosition, + programCache: this.programCache, + pass: 0, + filterBackend: this, + targetCanvas: targetCanvas + }; + var tempFbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); + filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); }); + resizeCanvasIfNeeded(pipelineState); + this.copyGLTo2D(gl, pipelineState); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.deleteTexture(pipelineState.sourceTexture); + gl.deleteTexture(pipelineState.targetTexture); + gl.deleteFramebuffer(tempFbo); + targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); + return pipelineState; + }, + + /** + * Detach event listeners, remove references, and clean up caches. + */ + dispose: function() { + if (this.canvas) { + this.canvas = null; + this.gl = null; + } + this.clearWebGLCaches(); + }, + + /** + * Wipe out WebGL-related caches. + */ + clearWebGLCaches: function() { + this.programCache = {}; + this.textureCache = {}; + }, + + /** + * Create a WebGL texture object. + * + * Accepts specific dimensions to initialize the texture to or a source image. + * + * @param {WebGLRenderingContext} gl The GL context to use for creating the texture. + * @param {Number} width The width to initialize the texture at. + * @param {Number} height The height to initialize the texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. + * @returns {WebGLTexture} + */ + createTexture: function(gl, width, height, textureImageSource) { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (textureImageSource) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImageSource); + } + else { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + return texture; + }, + + /** + * Can be optionally used to get a texture from the cache array + * + * If an existing texture is not found, a new texture is created and cached. + * + * @param {String} uniqueId A cache key to use to find an existing texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source to use to create the + * texture cache entry if one does not already exist. + */ + getCachedTexture: function(uniqueId, textureImageSource) { + if (this.textureCache[uniqueId]) { + return this.textureCache[uniqueId]; + } + else { + var texture = this.createTexture( + this.gl, textureImageSource.width, textureImageSource.height, textureImageSource); + this.textureCache[uniqueId] = texture; + return texture; + } + }, + + /** + * Clear out cached resources related to a source image that has been + * filtered previously. + * + * @param {String} cacheKey The cache key provided when the source image was filtered. + */ + evictCachesForKey: function(cacheKey) { + if (this.textureCache[cacheKey]) { + this.gl.deleteTexture(this.textureCache[cacheKey]); + delete this.textureCache[cacheKey]; + } + }, + + copyGLTo2D: copyGLTo2DDrawImage, + + /** + * Attempt to extract GPU information strings from a WebGL context. + * + * Useful information when debugging or blacklisting specific GPUs. + * + * @returns {Object} A GPU info object with renderer and vendor strings. + */ + captureGPUInfo: function() { + if (this.gpuInfo) { + return this.gpuInfo; + } + var gl = this.gl, gpuInfo = { renderer: '', vendor: '' }; + if (!gl) { + return gpuInfo; + } + var ext = gl.getExtension('WEBGL_debug_renderer_info'); + if (ext) { + var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); + var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); + if (renderer) { + gpuInfo.renderer = renderer.toLowerCase(); + } + if (vendor) { + gpuInfo.vendor = vendor.toLowerCase(); + } + } + this.gpuInfo = gpuInfo; + return gpuInfo; + }, + }; +})(); + +function resizeCanvasIfNeeded(pipelineState) { + var targetCanvas = pipelineState.targetCanvas, + width = targetCanvas.width, height = targetCanvas.height, + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight; + + if (width !== dWidth || height !== dHeight) { + targetCanvas.width = dWidth; + targetCanvas.height = dHeight; + } +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas. + * + * The WebGL canvas is assumed to be upside down, with the top-left pixel of the + * desired output image appearing in the bottom-left corner of the WebGL canvas. + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DDrawImage(gl, pipelineState) { + var glCanvas = gl.canvas, targetCanvas = pipelineState.targetCanvas, + ctx = targetCanvas.getContext('2d'); + ctx.translate(0, targetCanvas.height); // move it down again + ctx.scale(1, -1); // vertical flip + // where is my image on the big glcanvas? + var sourceY = glCanvas.height - targetCanvas.height; + ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, + targetCanvas.width, targetCanvas.height); +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData + * API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra). + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DPutImageData(gl, pipelineState) { + var targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext('2d'), + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight, + numBytes = dWidth * dHeight * 4; + + // eslint-disable-next-line no-undef + var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); + // eslint-disable-next-line no-undef + var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); + + gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8); + var imgData = new ImageData(u8Clamped, dWidth, dHeight); + ctx.putImageData(imgData, 0, 0); +} + + +(function() { + + 'use strict'; + + var noop = function() {}; + + fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; + + /** + * Canvas 2D filter backend. + */ + function Canvas2dFilterBackend() {}; + + Canvas2dFilterBackend.prototype = /** @lends fabric.Canvas2dFilterBackend.prototype */ { + evictCachesForKey: noop, + dispose: noop, + clearWebGLCaches: noop, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Apply a set of filters against a source image and draw the filtered output + * to the provided destination canvas. + * + * @param {EnhancedFilter} filters The filter to apply. + * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. + * @param {Number} sourceWidth The width of the source input. + * @param {Number} sourceHeight The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + */ + applyFilters: function(filters, sourceElement, sourceWidth, sourceHeight, targetCanvas) { + var ctx = targetCanvas.getContext('2d'); + ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); + var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var pipelineState = { + sourceWidth: sourceWidth, + sourceHeight: sourceHeight, + imageData: imageData, + originalEl: sourceElement, + originalImageData: originalImageData, + canvasEl: targetCanvas, + ctx: ctx, + filterBackend: this, + }; + filters.forEach(function(filter) { filter.applyTo(pipelineState); }); + if (pipelineState.imageData.width !== sourceWidth || pipelineState.imageData.height !== sourceHeight) { + targetCanvas.width = pipelineState.imageData.width; + targetCanvas.height = pipelineState.imageData.height; + } + ctx.putImageData(pipelineState.imageData, 0, 0); + return pipelineState; + }, + + }; +})(); + + +/** + * @namespace fabric.Image.filters + * @memberOf fabric.Image + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + */ +fabric.Image = fabric.Image || { }; +fabric.Image.filters = fabric.Image.filters || { }; + +/** + * Root filter class from which all filter classes inherit from + * @class fabric.Image.filters.BaseFilter + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'BaseFilter', + + /** + * Array of attributes to send with buffers. do not modify + * @private + */ + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + fragmentSource: 'precision highp float;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform sampler2D uTexture;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + '}', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Sets filter's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Compile this filter's shader program. + * + * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. + * @param {String} fragmentSource fragmentShader source for compilation + * @param {String} vertexSource vertexShader source for compilation + */ + createProgram: function(gl, fragmentSource, vertexSource) { + fragmentSource = fragmentSource || this.fragmentSource; + vertexSource = vertexSource || this.vertexSource; + if (fabric.webGlPrecision !== 'highp'){ + fragmentSource = fragmentSource.replace( + /precision highp float/g, + 'precision ' + fabric.webGlPrecision + ' float' + ); + } + var vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Vertex shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(vertexShader) + ); + } + + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Fragment shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(fragmentShader) + ); + } + + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Shader link error for "${this.type}" ' + + gl.getProgramInfoLog(program) + ); + } + + var attributeLocations = this.getAttributeLocations(gl, program); + var uniformLocations = this.getUniformLocations(gl, program) || { }; + uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); + uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); + return { + program: program, + attributeLocations: attributeLocations, + uniformLocations: uniformLocations + }; + }, + + /** + * Return a map of attribute names to WebGLAttributeLocation objects. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. + * @returns {Object} A map of attribute names to attribute locations. + */ + getAttributeLocations: function(gl, program) { + return { + aPosition: gl.getAttribLocation(program, 'aPosition'), + }; + }, + + /** + * Return a map of uniform names to WebGLUniformLocation objects. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. + * @returns {Object} A map of uniform names to uniform locations. + */ + getUniformLocations: function (/* gl, program */) { + // in case i do not need any special uniform i need to return an empty object + return { }; + }, + + /** + * Send attribute data from this filter to its shader program on the GPU. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} attributeLocations A map of shader attribute names to their locations. + */ + sendAttributeData: function(gl, attributeLocations, aPositionData) { + var attributeLocation = attributeLocations.aPosition; + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(attributeLocation); + gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); + }, + + _setupFrameBuffer: function(options) { + var gl = options.context, width, height; + if (options.passes > 1) { + width = options.destinationWidth; + height = options.destinationHeight; + if (options.sourceWidth !== width || options.sourceHeight !== height) { + gl.deleteTexture(options.targetTexture); + options.targetTexture = options.filterBackend.createTexture(gl, width, height); + } + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, + options.targetTexture, 0); + } + else { + // draw last filter on canvas and not to framebuffer. + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.finish(); + } + }, + + _swapTextures: function(options) { + options.passes--; + options.pass++; + var temp = options.targetTexture; + options.targetTexture = options.sourceTexture; + options.sourceTexture = temp; + }, + + /** + * Generic isNeutral implementation for one parameter based filters. + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) + * @param {Object} options + **/ + isNeutralState: function(/* options */) { + var main = this.mainParameter, + _class = fabric.Image.filters[this.type].prototype; + if (main) { + if (Array.isArray(_class[main])) { + for (var i = _class[main].length; i--;) { + if (this[main][i] !== _class[main][i]) { + return false; + } + } + return true; + } + else { + return _class[main] === this[main]; + } + } + else { + return false; + } + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + if (!options.programCache.hasOwnProperty(this.type)) { + options.programCache[this.type] = this.createProgram(options.context); + } + return options.programCache[this.type]; + }, + + /** + * Apply this filter using webgl. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.originalTexture The texture of the original input image. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyToWebGL: function(options) { + var gl = options.context; + var shader = this.retrieveShader(options); + if (options.pass === 0 && options.originalTexture) { + gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); + } + else { + gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); + } + gl.useProgram(shader.program); + this.sendAttributeData(gl, shader.attributeLocations, options.aPosition); + + gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); + gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); + + this.sendUniformData(gl, shader.uniformLocations); + gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + + bindAdditionalTexture: function(gl, texture, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, texture); + // reset active texture to 0 as usual + gl.activeTexture(gl.TEXTURE0); + }, + + unbindAdditionalTexture: function(gl, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.activeTexture(gl.TEXTURE0); + }, + + getMainParameter: function() { + return this[this.mainParameter]; + }, + + setMainParameter: function(value) { + this[this.mainParameter] = value; + }, + + /** + * Send uniform data from this filter to its shader program on the GPU. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} uniformLocations A map of shader uniform names to their locations. + */ + sendUniformData: function(/* gl, uniformLocations */) { + // Intentionally left blank. Override me in subclasses. + }, + + /** + * If needed by a 2d filter, this functions can create an helper canvas to be used + * remember that options.targetCanvas is available for use till end of chain. + */ + createHelpLayer: function(options) { + if (!options.helpLayer) { + var helpLayer = document.createElement('canvas'); + helpLayer.width = options.sourceWidth; + helpLayer.height = options.sourceHeight; + options.helpLayer = helpLayer; + } + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + var object = { type: this.type }, mainP = this.mainParameter; + if (mainP) { + object[mainP] = this[mainP]; + } + return object; + }, + + /** + * Returns a JSON representation of an instance + * @return {Object} JSON + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + } +}); + +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; +}; + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Matrix filter class + * @class fabric.Image.filters.ColorMatrix + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php} + * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl} + * @example Kodachrome filter + * var filter = new fabric.Image.filters.ColorMatrix({ + * matrix: [ + 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502, + -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203, + -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946, + 0, 0, 0, 1, 0 + ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'ColorMatrix', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform mat4 uColorMatrix;\n' + + 'uniform vec4 uConstants;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color *= uColorMatrix;\n' + + 'color += uConstants;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Colormatrix for pixels. + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ], + + mainParameter: 'matrix', + + /** + * Lock the colormatrix on the color part, skipping alpha, manly for non webgl scenario + * to save some calculation + */ + colorsOnly: true, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.matrix = this.matrix.slice(0); + }, + + /** + * Apply the ColorMatrix operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = data.length, + m = this.matrix, + r, g, b, a, i, colorsOnly = this.colorsOnly; + + for (i = 0; i < iLen; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + if (colorsOnly) { + data[i] = r * m[0] + g * m[1] + b * m[2] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + m[14] * 255; + } + else { + a = data[i + 3]; + data[i] = r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255; + data[i + 3] = r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'), + uConstants: gl.getUniformLocation(program, 'uConstants'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var m = this.matrix, + matrix = [ + m[0], m[1], m[2], m[3], + m[5], m[6], m[7], m[8], + m[10], m[11], m[12], m[13], + m[15], m[16], m[17], m[18] + ], + constants = [m[4], m[9], m[14], m[19]]; + gl.uniformMatrix4fv(uniformLocations.uColorMatrix, false, matrix); + gl.uniform4fv(uniformLocations.uConstants, constants); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + */ + fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Brightness filter class + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 0.05 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Brightness', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBrightness;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += uBrightness;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Brightness value, from -1 to 1. + * translated to -255 to 255 for 2d + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Number} brightness + * @default + */ + brightness: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'brightness', + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.brightness === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + brightness = Math.round(this.brightness * 255); + for (i = 0; i < len; i += 4) { + data[i] = data[i] + brightness; + data[i + 1] = data[i + 1] + brightness; + data[i + 2] = data[i + 2] + brightness; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBrightness: gl.getUniformLocation(program, 'uBrightness'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBrightness, this.brightness); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + */ + fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Adapted from html5rocks article + * @class fabric.Image.filters.Convolute + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Convolute', + + /* + * Opaque value (true/false) + */ + opaque: false, + + /* + * matrix for the filter, max 9x9 + */ + matrix: [0, 0, 0, 0, 1, 0, 0, 0, 0], + + /** + * Fragment source for the brightness program + */ + fragmentSource: { + Convolute_3_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_3_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_5_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_5_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_7_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_7_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_9_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_9_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + }, + + /** + * Constructor + * @memberOf fabric.Image.filters.Convolute.prototype + * @param {Object} [options] Options object + * @param {Boolean} [options.opaque=false] Opaque value (true/false) + * @param {Array} [options.matrix] Filter matrix + */ + + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var size = Math.sqrt(this.matrix.length); + var cacheKey = this.type + '_' + size + '_' + (this.opaque ? 1 : 0); + var shaderSource = this.fragmentSource[cacheKey]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + weights = this.matrix, + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side / 2), + sw = imageData.width, + sh = imageData.height, + output = options.ctx.createImageData(sw, sh), + dst = output.data, + // go through the destination image pixels + alphaFac = this.opaque ? 1 : 0, + r, g, b, a, dstOff, + scx, scy, srcOff, wt, + x, y, cx, cy; + + for (y = 0; y < sh; y++) { + for (x = 0; x < sw; x++) { + dstOff = (y * sw + x) * 4; + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + r = 0; g = 0; b = 0; a = 0; + + for (cy = 0; cy < side; cy++) { + for (cx = 0; cx < side; cx++) { + scy = y + cy - halfSide; + scx = x + cx - halfSide; + + // eslint-disable-next-line max-depth + if (scy < 0 || scy >= sh || scx < 0 || scx >= sw) { + continue; + } + + srcOff = (scy * sw + scx) * 4; + wt = weights[cy * side + cx]; + + r += data[srcOff] * wt; + g += data[srcOff + 1] * wt; + b += data[srcOff + 2] * wt; + // eslint-disable-next-line max-depth + if (!alphaFac) { + a += data[srcOff + 3] * wt; + } + } + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + if (!alphaFac) { + dst[dstOff + 3] = a; + } + else { + dst[dstOff + 3] = data[dstOff + 3]; + } + } + } + options.imageData = output; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMatrix: gl.getUniformLocation(program, 'uMatrix'), + uOpaque: gl.getUniformLocation(program, 'uOpaque'), + uHalfSize: gl.getUniformLocation(program, 'uHalfSize'), + uSize: gl.getUniformLocation(program, 'uSize'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1fv(uniformLocations.uMatrix, this.matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + opaque: this.opaque, + matrix: this.matrix + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + */ + fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Grayscale image filter class + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Grayscale', + + fragmentSource: { + average: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float average = (color.r + color.b + color.g) / 3.0;\n' + + 'gl_FragColor = vec4(average, average, average, color.a);\n' + + '}', + lightness: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + luminosity: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + }, + + + /** + * Grayscale mode, between 'average', 'lightness', 'luminosity' + * @param {String} type + * @default + */ + mode: 'average', + + mainParameter: 'mode', + + /** + * Apply the Grayscale operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length, value, + mode = this.mode; + for (i = 0; i < len; i += 4) { + if (mode === 'average') { + value = (data[i] + data[i + 1] + data[i + 2]) / 3; + } + else if (mode === 'lightness') { + value = (Math.min(data[i], data[i + 1], data[i + 2]) + + Math.max(data[i], data[i + 1], data[i + 2])) / 2; + } + else if (mode === 'luminosity') { + value = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2]; + } + data[i] = value; + data[i + 1] = value; + data[i + 2] = value; + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var shaderSource = this.fragmentSource[this.mode]; + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMode: gl.getUniformLocation(program, 'uMode'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + // default average mode. + var mode = 1; + gl.uniform1i(uniformLocations.uMode, mode); + }, + + /** + * Grayscale filter isNeutralState implementation + * The filter is never neutral + * on the image + **/ + isNeutralState: function() { + return false; + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + */ + fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Invert filter class + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Invert = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Invert', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uInvert;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'if (uInvert == 1) {\n' + + 'gl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n' + + '} else {\n' + + 'gl_FragColor = color;\n' + + '}\n' + + '}', + + /** + * Filter invert. if false, does nothing + * @param {Boolean} invert + * @default + */ + invert: true, + + mainParameter: 'invert', + + /** + * Apply the Invert operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length; + for (i = 0; i < len; i += 4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + }, + + /** + * Invert filter isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function() { + return !this.invert; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uInvert: gl.getUniformLocation(program, 'uInvert'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1i(uniformLocations.uInvert, this.invert); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + */ + fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; + + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Noise filter class + * @class fabric.Image.filters.Noise + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Noise', + + /** + * Fragment source for the noise program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uStepH;\n' + + 'uniform float uNoise;\n' + + 'uniform float uSeed;\n' + + 'varying vec2 vTexCoord;\n' + + 'float rand(vec2 co, float seed, float vScale) {\n' + + 'return fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'noise', + + /** + * Noise value, from + * @param {Number} noise + * @default + */ + noise: 0, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.noise === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + noise = this.noise, rand; + + for (i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uNoise: gl.getUniformLocation(program, 'uNoise'), + uSeed: gl.getUniformLocation(program, 'uSeed'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uNoise, this.noise / 255); + gl.uniform1f(uniformLocations.uSeed, Math.random()); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + noise: this.noise + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + */ + fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Pixelate filter class + * @class fabric.Image.filters.Pixelate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Pixelate', + + blocksize: 4, + + mainParameter: 'blocksize', + + /** + * Fragment source for the Pixelate program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBlocksize;\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'float blockW = uBlocksize * uStepW;\n' + + 'float blockH = uBlocksize * uStepW;\n' + + 'int posX = int(vTexCoord.x / blockW);\n' + + 'int posY = int(vTexCoord.y / blockH);\n' + + 'float fposX = float(posX);\n' + + 'float fposY = float(posY);\n' + + 'vec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\n' + + 'vec4 color = texture2D(uTexture, squareCoords);\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Apply the Pixelate operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = imageData.height, + jLen = imageData.width, + index, i, j, r, g, b, a, + _i, _j, _iLen, _jLen; + + for (i = 0; i < iLen; i += this.blocksize) { + for (j = 0; j < jLen; j += this.blocksize) { + + index = (i * 4) * jLen + (j * 4); + + r = data[index]; + g = data[index + 1]; + b = data[index + 2]; + a = data[index + 3]; + + _iLen = Math.min(i + this.blocksize, iLen); + _jLen = Math.min(j + this.blocksize, jLen); + for (_i = i; _i < _iLen; _i++) { + for (_j = j; _j < _jLen; _j++) { + index = (_i * 4) * jLen + (_j * 4); + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = a; + } + } + } + } + }, + + /** + * Indicate when the filter is not gonna apply changes to the image + **/ + isNeutralState: function() { + return this.blocksize === 1; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBlocksize: gl.getUniformLocation(program, 'uBlocksize'), + uStepW: gl.getUniformLocation(program, 'uStepW'), + uStepH: gl.getUniformLocation(program, 'uStepH'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBlocksize, this.blocksize); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + */ + fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Remove white filter class + * @class fabric.Image.filters.RemoveColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveColor#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveColor({ + * threshold: 0.2, + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'RemoveColor', + + /** + * Color to remove, in any format understood by fabric.Color. + * @param {String} type + * @default + */ + color: '#FFFFFF', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uLow;\n' + + 'uniform vec4 uHigh;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + 'if(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\n' + + 'gl_FragColor.a = 0.0;\n' + + '}\n' + + '}', + + /** + * distance to actual color, as value up or down from each r,g,b + * between 0 and 1 + **/ + distance: 0.02, + + /** + * For color to remove inside distance, use alpha channel for a smoother deletion + * NOT IMPLEMENTED YET + **/ + useAlpha: false, + + /** + * Constructor + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + * @param {Number} [options.color=#RRGGBB] Threshold value + * @param {Number} [options.distance=10] Distance value + */ + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + distance = this.distance * 255, + r, g, b, + source = new fabric.Color(this.color).getSource(), + lowC = [ + source[0] - distance, + source[1] - distance, + source[2] - distance, + ], + highC = [ + source[0] + distance, + source[1] + distance, + source[2] + distance, + ]; + + + for (i = 0; i < data.length; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + if (r > lowC[0] && + g > lowC[1] && + b > lowC[2] && + r < highC[0] && + g < highC[1] && + b < highC[2]) { + data[i + 3] = 0; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uLow: gl.getUniformLocation(program, 'uLow'), + uHigh: gl.getUniformLocation(program, 'uHigh'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(), + distance = parseFloat(this.distance), + lowC = [ + 0 + source[0] / 255 - distance, + 0 + source[1] / 255 - distance, + 0 + source[2] / 255 - distance, + 1 + ], + highC = [ + source[0] / 255 + distance, + source[1] / 255 + distance, + source[2] / 255 + distance, + 1 + ]; + gl.uniform4fv(uniformLocations.uLow, lowC); + gl.uniform4fv(uniformLocations.uHigh, highC); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + color: this.color, + distance: this.distance + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite + */ + fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + var matrices = { + Brownie: [ + 0.59970,0.34553,-0.27082,0,0.186, + -0.03770,0.86095,0.15059,0,-0.1449, + 0.24113,-0.07441,0.44972,0,-0.02965, + 0,0,0,1,0 + ], + Vintage: [ + 0.62793,0.32021,-0.03965,0,0.03784, + 0.02578,0.64411,0.03259,0,0.02926, + 0.04660,-0.08512,0.52416,0,0.02023, + 0,0,0,1,0 + ], + Kodachrome: [ + 1.12855,-0.39673,-0.03992,0,0.24991, + -0.16404,1.08352,-0.05498,0,0.09698, + -0.16786,-0.56034,1.60148,0,0.13972, + 0,0,0,1,0 + ], + Technicolor: [ + 1.91252,-0.85453,-0.09155,0,0.04624, + -0.30878,1.76589,-0.10601,0,-0.27589, + -0.23110,-0.75018,1.84759,0,0.12137, + 0,0,0,1,0 + ], + Polaroid: [ + 1.438,-0.062,-0.062,0,0, + -0.122,1.378,-0.122,0,0, + -0.016,-0.016,1.483,0,0, + 0,0,0,1,0 + ], + Sepia: [ + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0 + ], + BlackWhite: [ + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 0, 0, 0, 1, 0, + ] + }; + + for (var key in matrices) { + filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: key, + + /** + * Colormatrix for the effect + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: matrices[key], + + /** + * Lock the matrix export for this kind of static, parameter less filters. + */ + mainParameter: false, + /** + * Lock the colormatrix on the color part, skipping alpha + */ + colorsOnly: true, + + }); + fabric.Image.filters[key].fromObject = fabric.Image.filters.BaseFilter.fromObject; + } +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Blend filter class + * @class fabric.Image.filter.BlendColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { + type: 'BlendColor', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + color: '#F95C63', + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend color operation. + **/ + alpha: 1, + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'gl_FragColor.rgb *= uColor.rgb;\n', + screen: 'gl_FragColor.rgb = 1.0 - (1.0 - gl_FragColor.rgb) * (1.0 - uColor.rgb);\n', + add: 'gl_FragColor.rgb += uColor.rgb;\n', + diff: 'gl_FragColor.rgb = abs(gl_FragColor.rgb - uColor.rgb);\n', + subtract: 'gl_FragColor.rgb -= uColor.rgb;\n', + lighten: 'gl_FragColor.rgb = max(gl_FragColor.rgb, uColor.rgb);\n', + darken: 'gl_FragColor.rgb = min(gl_FragColor.rgb, uColor.rgb);\n', + exclusion: 'gl_FragColor.rgb += uColor.rgb - 2.0 * (uColor.rgb * gl_FragColor.rgb);\n', + overlay: 'if (uColor.r < 0.5) {\n' + + 'gl_FragColor.r *= 2.0 * uColor.r;\n' + + '} else {\n' + + 'gl_FragColor.r = 1.0 - 2.0 * (1.0 - gl_FragColor.r) * (1.0 - uColor.r);\n' + + '}\n' + + 'if (uColor.g < 0.5) {\n' + + 'gl_FragColor.g *= 2.0 * uColor.g;\n' + + '} else {\n' + + 'gl_FragColor.g = 1.0 - 2.0 * (1.0 - gl_FragColor.g) * (1.0 - uColor.g);\n' + + '}\n' + + 'if (uColor.b < 0.5) {\n' + + 'gl_FragColor.b *= 2.0 * uColor.b;\n' + + '} else {\n' + + 'gl_FragColor.b = 1.0 - 2.0 * (1.0 - gl_FragColor.b) * (1.0 - uColor.b);\n' + + '}\n', + tint: 'gl_FragColor.rgb *= (1.0 - uColor.a);\n' + + 'gl_FragColor.rgb += uColor.rgb;\n', + }, + + /** + * build the fragment source for the filters, joining the common part with + * the specific one. + * @param {String} mode the mode of the filter, a key of this.fragmentSource + * @return {String} the source to be compiled + * @private + */ + buildSource: function(mode) { + return 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'gl_FragColor = color;\n' + + 'if (color.a > 0.0) {\n' + + this.fragmentSource[mode] + + '}\n' + + '}'; + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode, shaderSource; + if (!options.programCache.hasOwnProperty(cacheKey)) { + shaderSource = this.buildSource(this.mode); + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, iLen = data.length, + tr, tg, tb, + r, g, b, + source, alpha1 = 1 - this.alpha; + + source = new fabric.Color(this.color).getSource(); + tr = source[0] * this.alpha; + tg = source[1] * this.alpha; + tb = source[2] * this.alpha; + + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + break; + case 'screen': + data[i] = 255 - (255 - r) * (255 - tr) / 255; + data[i + 1] = 255 - (255 - g) * (255 - tg) / 255; + data[i + 2] = 255 - (255 - b) * (255 - tb) / 255; + break; + case 'add': + data[i] = r + tr; + data[i + 1] = g + tg; + data[i + 2] = b + tb; + break; + case 'diff': + case 'difference': + data[i] = Math.abs(r - tr); + data[i + 1] = Math.abs(g - tg); + data[i + 2] = Math.abs(b - tb); + break; + case 'subtract': + data[i] = r - tr; + data[i + 1] = g - tg; + data[i + 2] = b - tb; + break; + case 'darken': + data[i] = Math.min(r, tr); + data[i + 1] = Math.min(g, tg); + data[i + 2] = Math.min(b, tb); + break; + case 'lighten': + data[i] = Math.max(r, tr); + data[i + 1] = Math.max(g, tg); + data[i + 2] = Math.max(b, tb); + break; + case 'overlay': + data[i] = tr < 128 ? (2 * r * tr / 255) : (255 - 2 * (255 - r) * (255 - tr) / 255); + data[i + 1] = tg < 128 ? (2 * g * tg / 255) : (255 - 2 * (255 - g) * (255 - tg) / 255); + data[i + 2] = tb < 128 ? (2 * b * tb / 255) : (255 - 2 * (255 - b) * (255 - tb) / 255); + break; + case 'exclusion': + data[i] = tr + r - ((2 * tr * r) / 255); + data[i + 1] = tg + g - ((2 * tg * g) / 255); + data[i + 2] = tb + b - ((2 * tb * b) / 255); + break; + case 'tint': + data[i] = tr + r * alpha1; + data[i + 1] = tg + g * alpha1; + data[i + 2] = tb + b * alpha1; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColor: gl.getUniformLocation(program, 'uColor'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(); + source[0] = this.alpha * source[0] / 255; + source[1] = this.alpha * source[1] / 255; + source[2] = this.alpha * source[2] / 255; + source[3] = this.alpha; + gl.uniform4fv(uniformLocations.uColor, source); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + color: this.color, + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor + */ + fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Image Blend filter class + * @class fabric.Image.filter.BlendImage + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendImage = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.BlendImage.prototype */ { + type: 'BlendImage', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + image: null, + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend image operation. + * not implemented. + **/ + alpha: 1, + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'uniform mat3 uTransformMatrix;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'vTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.rgba *= color2.rgba;\n' + + 'gl_FragColor = color;\n' + + '}', + mask: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.a = color2.a;\n' + + 'gl_FragColor = color;\n' + + '}', + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + var shaderSource = this.fragmentSource[this.mode]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + applyToWebGL: function(options) { + // load texture to blend. + var gl = options.context, + texture = this.createTexture(options.filterBackend, this.image); + this.bindAdditionalTexture(gl, texture, gl.TEXTURE1); + this.callSuper('applyToWebGL', options); + this.unbindAdditionalTexture(gl, gl.TEXTURE1); + }, + + createTexture: function(backend, image) { + return backend.getCachedTexture(image.cacheKey, image._element); + }, + + /** + * Calculate a transformMatrix to adapt the image to blend over + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + calculateMatrix: function() { + var image = this.image, + width = image._element.width, + height = image._element.height; + return [ + 1 / image.scaleX, 0, 0, + 0, 1 / image.scaleY, 0, + -image.left / width, -image.top / height, 1 + ]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + resources = options.filterBackend.resources, + data = imageData.data, iLen = data.length, + width = imageData.width, + height = imageData.height, + tr, tg, tb, ta, + r, g, b, a, + canvas1, context, image = this.image, blendData; + + if (!resources.blendImage) { + resources.blendImage = fabric.util.createCanvasElement(); + } + canvas1 = resources.blendImage; + context = canvas1.getContext('2d'); + if (canvas1.width !== width || canvas1.height !== height) { + canvas1.width = width; + canvas1.height = height; + } + else { + context.clearRect(0, 0, width, height); + } + context.setTransform(image.scaleX, 0, 0, image.scaleY, image.left, image.top); + context.drawImage(image._element, 0, 0, width, height); + blendData = context.getImageData(0, 0, width, height).data; + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + + tr = blendData[i]; + tg = blendData[i + 1]; + tb = blendData[i + 2]; + ta = blendData[i + 3]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + data[i + 3] = a * ta / 255; + break; + case 'mask': + data[i + 3] = ta; + break; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uTransformMatrix: gl.getUniformLocation(program, 'uTransformMatrix'), + uImage: gl.getUniformLocation(program, 'uImage'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var matrix = this.calculateMatrix(); + gl.uniform1i(uniformLocations.uImage, 1); // texture unit 1. + gl.uniformMatrix3fv(uniformLocations.uTransformMatrix, false, matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + image: this.image && this.image.toObject(), + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} callback to be invoked after filter creation + * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage + */ + fabric.Image.filters.BlendImage.fromObject = function(object, callback) { + fabric.Image.fromObject(object.image, function(image) { + var options = fabric.util.object.clone(object); + options.image = image; + callback(new fabric.Image.filters.BlendImage(options)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, + sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin, + ceil = Math.ceil, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Resize image filter class + * @class fabric.Image.filters.Resize + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Resize(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Resize', + + /** + * Resize type + * for webgl resizeType is just lanczos, for canvas2d can be: + * bilinear, hermite, sliceHack, lanczos. + * @param {String} resizeType + * @default + */ + resizeType: 'hermite', + + /** + * Scale factor for resizing, x axis + * @param {Number} scaleX + * @default + */ + scaleX: 1, + + /** + * Scale factor for resizing, y axis + * @param {Number} scaleY + * @default + */ + scaleY: 1, + + /** + * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos + * @param {Number} lanczosLobes + * @default + */ + lanczosLobes: 3, + + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uDelta: gl.getUniformLocation(program, 'uDelta'), + uTaps: gl.getUniformLocation(program, 'uTaps'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform2fv(uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height]); + gl.uniform1fv(uniformLocations.uTaps, this.taps); + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var filterWindow = this.getFilterWindow(), cacheKey = this.type + '_' + filterWindow; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var fragmentShader = this.generateShader(filterWindow); + options.programCache[cacheKey] = this.createProgram(options.context, fragmentShader); + } + return options.programCache[cacheKey]; + }, + + getFilterWindow: function() { + var scale = this.tempScale; + return Math.ceil(this.lanczosLobes / scale); + }, + + getTaps: function() { + var lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, + filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); + for (var i = 1; i <= filterWindow; i++) { + taps[i - 1] = lobeFunction(i * scale); + } + return taps; + }, + + /** + * Generate vertex and shader sources from the necessary steps numbers + * @param {Number} filterWindow + */ + generateShader: function(filterWindow) { + var offsets = new Array(filterWindow), + fragmentShader = this.fragmentSourceTOP, filterWindow; + + for (var i = 1; i <= filterWindow; i++) { + offsets[i - 1] = i + '.0 * uDelta'; + } + + fragmentShader += 'uniform float uTaps[' + filterWindow + '];\n'; + fragmentShader += 'void main() {\n'; + fragmentShader += ' vec4 color = texture2D(uTexture, vTexCoord);\n'; + fragmentShader += ' float sum = 1.0;\n'; + + offsets.forEach(function(offset, i) { + fragmentShader += ' color += texture2D(uTexture, vTexCoord + ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' color += texture2D(uTexture, vTexCoord - ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' sum += 2.0 * uTaps[' + i + '];\n'; + }); + fragmentShader += ' gl_FragColor = color / sum;\n'; + fragmentShader += '}'; + return fragmentShader; + }, + + fragmentSourceTOP: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n', + + /** + * Apply the resize filter to the image + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + options.passes++; + this.width = options.sourceWidth; + this.horizontal = true; + this.dW = Math.round(this.width * this.scaleX); + this.dH = options.sourceHeight; + this.tempScale = this.dW / this.width; + this.taps = this.getTaps(); + options.destinationWidth = this.dW; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceWidth = options.destinationWidth; + + this.height = options.sourceHeight; + this.horizontal = false; + this.dH = Math.round(this.height * this.scaleY); + this.tempScale = this.dH / this.height; + this.taps = this.getTaps(); + options.destinationHeight = this.dH; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceHeight = options.destinationHeight; + } + else { + this.applyTo2d(options); + } + }, + + isNeutralState: function() { + return this.scaleX === 1 && this.scaleY === 1; + }, + + lanczosCreate: function(lobes) { + return function(x) { + if (x >= lobes || x <= -lobes) { + return 0.0; + } + if (x < 1.19209290E-07 && x > -1.19209290E-07) { + return 1.0; + } + x *= Math.PI; + var xx = x / lobes; + return (sin(x) / x) * sin(xx) / xx; + }; + }, + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Resize.prototype + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} scaleX + * @param {Number} scaleY + */ + applyTo2d: function(options) { + var imageData = options.imageData, + scaleX = this.scaleX, + scaleY = this.scaleY; + + this.rcpScaleX = 1 / scaleX; + this.rcpScaleY = 1 / scaleY; + + var oW = imageData.width, oH = imageData.height, + dW = round(oW * scaleX), dH = round(oH * scaleY), + newData; + + if (this.resizeType === 'sliceHack') { + newData = this.sliceByTwo(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'hermite') { + newData = this.hermiteFastResize(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'bilinear') { + newData = this.bilinearFiltering(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'lanczos') { + newData = this.lanczosResize(options, oW, oH, dW, dH); + } + options.imageData = newData; + }, + + /** + * Filter sliceByTwo + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + sliceByTwo: function(options, oW, oH, dW, dH) { + var imageData = options.imageData, + mult = 0.5, doneW = false, doneH = false, stepW = oW * mult, + stepH = oH * mult, resources = fabric.filterBackend.resources, + tmpCanvas, ctx, sX = 0, sY = 0, dX = oW, dY = 0; + if (!resources.sliceByTwo) { + resources.sliceByTwo = document.createElement('canvas'); + } + tmpCanvas = resources.sliceByTwo; + if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) { + tmpCanvas.width = oW * 1.5; + tmpCanvas.height = oH; + } + ctx = tmpCanvas.getContext('2d'); + ctx.clearRect(0, 0, oW * 1.5, oH); + ctx.putImageData(imageData, 0, 0); + + dW = floor(dW); + dH = floor(dH); + + while (!doneW || !doneH) { + oW = stepW; + oH = stepH; + if (dW < floor(stepW * mult)) { + stepW = floor(stepW * mult); + } + else { + stepW = dW; + doneW = true; + } + if (dH < floor(stepH * mult)) { + stepH = floor(stepH * mult); + } + else { + stepH = dH; + doneH = true; + } + ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); + sX = dX; + sY = dY; + dY += stepH; + } + return ctx.getImageData(sX, sY, dW, dH); + }, + + /** + * Filter lanczosResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + lanczosResize: function(options, oW, oH, dW, dH) { + + function process(u) { + var v, i, weight, idx, a, red, green, + blue, alpha, fX, fY; + center.x = (u + 0.5) * ratioX; + icenter.x = floor(center.x); + for (v = 0; v < dH; v++) { + center.y = (v + 0.5) * ratioY; + icenter.y = floor(center.y); + a = 0; red = 0; green = 0; blue = 0; alpha = 0; + for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { + if (i < 0 || i >= oW) { + continue; + } + fX = floor(1000 * abs(i - center.x)); + if (!cacheLanc[fX]) { + cacheLanc[fX] = { }; + } + for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { + if (j < 0 || j >= oH) { + continue; + } + fY = floor(1000 * abs(j - center.y)); + if (!cacheLanc[fX][fY]) { + cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); + } + weight = cacheLanc[fX][fY]; + if (weight > 0) { + idx = (j * oW + i) * 4; + a += weight; + red += weight * srcData[idx]; + green += weight * srcData[idx + 1]; + blue += weight * srcData[idx + 2]; + alpha += weight * srcData[idx + 3]; + } + } + } + idx = (v * dW + u) * 4; + destData[idx] = red / a; + destData[idx + 1] = green / a; + destData[idx + 2] = blue / a; + destData[idx + 3] = alpha / a; + } + + if (++u < dW) { + return process(u); + } + else { + return destImg; + } + } + + var srcData = options.imageData.data, + destImg = options.ctx.createImageData(dW, dH), + destData = destImg.data, + lanczos = this.lanczosCreate(this.lanczosLobes), + ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, + rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, + range2X = ceil(ratioX * this.lanczosLobes / 2), + range2Y = ceil(ratioY * this.lanczosLobes / 2), + cacheLanc = { }, center = { }, icenter = { }; + + return process(0); + }, + + /** + * bilinearFiltering + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + bilinearFiltering: function(options, oW, oH, dW, dH) { + var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, + color, offset = 0, origPix, ratioX = this.rcpScaleX, + ratioY = this.rcpScaleY, + w4 = 4 * (oW - 1), img = options.imageData, + pixels = img.data, destImage = options.ctx.createImageData(dW, dH), + destPixels = destImage.data; + for (i = 0; i < dH; i++) { + for (j = 0; j < dW; j++) { + x = floor(ratioX * j); + y = floor(ratioY * i); + xDiff = ratioX * j - x; + yDiff = ratioY * i - y; + origPix = 4 * (y * oW + x); + + for (chnl = 0; chnl < 4; chnl++) { + a = pixels[origPix + chnl]; + b = pixels[origPix + 4 + chnl]; + c = pixels[origPix + w4 + chnl]; + d = pixels[origPix + w4 + 4 + chnl]; + color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; + destPixels[offset++] = color; + } + } + } + return destImage; + }, + + /** + * hermiteFastResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + hermiteFastResize: function(options, oW, oH, dW, dH) { + var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, + ratioWHalf = ceil(ratioW / 2), + ratioHHalf = ceil(ratioH / 2), + img = options.imageData, data = img.data, + img2 = options.ctx.createImageData(dW, dH), data2 = img2.data; + for (var j = 0; j < dH; j++) { + for (var i = 0; i < dW; i++) { + var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, + gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; + for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { + var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, + centerX = (i + 0.5) * ratioW, w0 = dy * dy; + for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { + var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, + w = sqrt(w0 + dx * dx); + /* eslint-disable max-depth */ + if (w > 1 && w < -1) { + continue; + } + //hermite filter + weight = 2 * w * w * w - 3 * w * w + 1; + if (weight > 0) { + dx = 4 * (xx + yy * oW); + //alpha + gxA += weight * data[dx + 3]; + weightsAlpha += weight; + //colors + if (data[dx + 3] < 255) { + weight = weight * data[dx + 3] / 250; + } + gxR += weight * data[dx]; + gxG += weight * data[dx + 1]; + gxB += weight * data[dx + 2]; + weights += weight; + } + /* eslint-enable max-depth */ + } + } + data2[x2] = gxR / weights; + data2[x2 + 1] = gxG / weights; + data2[x2 + 2] = gxB / weights; + data2[x2 + 3] = gxA / weightsAlpha; + } + } + return img2; + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + scaleX: this.scaleX, + scaleY: this.scaleY, + resizeType: this.resizeType, + lanczosLobes: this.lanczosLobes + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + */ + fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Contrast filter class + * @class fabric.Image.filters.Contrast + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Contrast({ + * contrast: 0.25 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Contrast', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uContrast;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\n' + + 'color.rgb = contrastF * (color.rgb - 0.5) + 0.5;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * contrast value, range from -1 to 1. + * @param {Number} contrast + * @default 0 + */ + contrast: 0, + + mainParameter: 'contrast', + + /** + * Constructor + * @memberOf fabric.Image.filters.Contrast.prototype + * @param {Object} [options] Options object + * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1) + */ + + /** + * Apply the Contrast operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + if (this.contrast === 0) { + return; + } + var imageData = options.imageData, i, len, + data = imageData.data, len = data.length, + contrast = Math.floor(this.contrast * 255), + contrastF = 259 * (contrast + 255) / (255 * (259 - contrast)); + + for (i = 0; i < len; i += 4) { + data[i] = contrastF * (data[i] - 128) + 128; + data[i + 1] = contrastF * (data[i + 1] - 128) + 128; + data[i + 2] = contrastF * (data[i + 2] - 128) + 128; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uContrast: gl.getUniformLocation(program, 'uContrast'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uContrast, this.contrast); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + */ + fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Saturate filter class + * @class fabric.Image.filters.Saturation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Saturation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Saturation({ + * saturation: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Saturation', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uSaturation;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float rgMax = max(color.r, color.g);\n' + + 'float rgbMax = max(rgMax, color.b);\n' + + 'color.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\n' + + 'color.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\n' + + 'color.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Saturation value, from -1 to 1. + * Increases/decreases the color saturation. + * A value of 0 has no effect. + * + * @param {Number} saturation + * @default + */ + saturation: 0, + + mainParameter: 'saturation', + + /** + * Constructor + * @memberOf fabric.Image.filters.Saturate.prototype + * @param {Object} [options] Options object + * @param {Number} [options.saturate=0] Value to saturate the image (-1...1) + */ + + /** + * Apply the Saturation operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.saturation === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.saturation, i, max; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + data[i] += max !== data[i] ? (max - data[i]) * adjust : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uSaturation: gl.getUniformLocation(program, 'uSaturation'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uSaturation, -this.saturation); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate + */ + fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Vibrance filter class + * @class fabric.Image.filters.Vibrance + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Vibrance#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Vibrance({ + * vibrance: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Vibrance = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Vibrance.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Vibrance', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uVibrance;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float max = max(color.r, max(color.g, color.b));\n' + + 'float avg = (color.r + color.g + color.b) / 3.0;\n' + + 'float amt = (abs(max - avg) * 2.0) * uVibrance;\n' + + 'color.r += max != color.r ? (max - color.r) * amt : 0.00;\n' + + 'color.g += max != color.g ? (max - color.g) * amt : 0.00;\n' + + 'color.b += max != color.b ? (max - color.b) * amt : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Vibrance value, from -1 to 1. + * Increases/decreases the saturation of more muted colors with less effect on saturated colors. + * A value of 0 has no effect. + * + * @param {Number} vibrance + * @default + */ + vibrance: 0, + + mainParameter: 'vibrance', + + /** + * Constructor + * @memberOf fabric.Image.filters.Vibrance.prototype + * @param {Object} [options] Options object + * @param {Number} [options.vibrance=0] Vibrance value for the image (between -1 and 1) + */ + + /** + * Apply the Vibrance operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.vibrance === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.vibrance, i, max, avg, amt; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + amt = ((Math.abs(max - avg) * 2 / 255) * adjust); + data[i] += max !== data[i] ? (max - data[i]) * amt : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * amt : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * amt : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uVibrance: gl.getUniformLocation(program, 'uVibrance'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uVibrance, -this.vibrance); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance + */ + fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Blur filter class + * @class fabric.Image.filters.Blur + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Blur#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Blur({ + * blur: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Blur = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blur.prototype */ { + + type: 'Blur', + + /* +'gl_FragColor = vec4(0.0);', +'gl_FragColor += texture2D(texture, vTexCoord + -7 * uDelta)*0.0044299121055113265;', +'gl_FragColor += texture2D(texture, vTexCoord + -6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + -5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + -4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + -3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + -2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + -1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord )*0.159576912161;', +'gl_FragColor += texture2D(texture, vTexCoord + 1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord + 2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + 3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + 4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + 5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + 6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + 7 * uDelta)*0.0044299121055113265;', +*/ + + /* eslint-disable max-len */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n' + + 'const float nSamples = 15.0;\n' + + 'vec3 v3offset = vec3(12.9898, 78.233, 151.7182);\n' + + 'float random(vec3 scale) {\n' + + /* use the fragment position for a different seed per-pixel */ + 'return fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = vec4(0.0);\n' + + 'float total = 0.0;\n' + + 'float offset = random(v3offset);\n' + + 'for (float t = -nSamples; t <= nSamples; t++) {\n' + + 'float percent = (t + offset - 0.5) / nSamples;\n' + + 'float weight = 1.0 - abs(percent);\n' + + 'color += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\n' + + 'total += weight;\n' + + '}\n' + + 'gl_FragColor = color / total;\n' + + '}', + /* eslint-enable max-len */ + + /** + * blur value, in percentage of image dimensions. + * specific to keep the image blur constant at different resolutions + * range between 0 and 1. + */ + blur: 0, + + mainParameter: 'blur', + + applyTo: function(options) { + if (options.webgl) { + // this aspectRatio is used to give the same blur to vertical and horizontal + this.aspectRatio = options.sourceWidth / options.sourceHeight; + options.passes++; + this._setupFrameBuffer(options); + this.horizontal = true; + this.applyToWebGL(options); + this._swapTextures(options); + this._setupFrameBuffer(options); + this.horizontal = false; + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + applyTo2d: function(options) { + // paint canvasEl with current image data. + //options.ctx.putImageData(options.imageData, 0, 0); + options.imageData = this.simpleBlur(options); + }, + + simpleBlur: function(options) { + var resources = options.filterBackend.resources, canvas1, canvas2, + width = options.imageData.width, + height = options.imageData.height; + + if (!resources.blurLayer1) { + resources.blurLayer1 = fabric.util.createCanvasElement(); + resources.blurLayer2 = fabric.util.createCanvasElement(); + } + canvas1 = resources.blurLayer1; + canvas2 = resources.blurLayer2; + if (canvas1.width !== width || canvas1.height !== height) { + canvas2.width = canvas1.width = width; + canvas2.height = canvas1.height = height; + } + var ctx1 = canvas1.getContext('2d'), + ctx2 = canvas2.getContext('2d'), + nSamples = 15, + random, percent, j, i, + blur = this.blur * 0.06 * 0.5; + + // load first canvas + ctx1.putImageData(options.imageData, 0, 0); + ctx2.clearRect(0, 0, width, height); + + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * width + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, j, random); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * height + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, random, j); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + options.ctx.drawImage(canvas1, 0, 0); + var newImageData = options.ctx.getImageData(0, 0, canvas1.width, canvas1.height); + ctx1.globalAlpha = 1; + ctx1.clearRect(0, 0, canvas1.width, canvas1.height); + return newImageData; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + delta: gl.getUniformLocation(program, 'uDelta'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var delta = this.chooseRightDelta(); + gl.uniform2fv(uniformLocations.delta, delta); + }, + + /** + * choose right value of image percentage to blur with + * @returns {Array} a numeric array with delta values + */ + chooseRightDelta: function() { + var blurScale = 1, delta = [0, 0], blur; + if (this.horizontal) { + if (this.aspectRatio > 1) { + // image is wide, i want to shrink radius horizontal + blurScale = 1 / this.aspectRatio; + } + } + else { + if (this.aspectRatio < 1) { + // image is tall, i want to shrink radius vertical + blurScale = this.aspectRatio; + } + } + blur = blurScale * this.blur * 0.12; + if (this.horizontal) { + delta[0] = blur; + } + else { + delta[1] = blur; + } + return delta; + }, + }); + + /** + * Deserialize a JSON definition of a BlurFilter into a concrete instance. + */ + filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Gamma filter class + * @class fabric.Image.filters.Gamma + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Gamma#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Gamma({ + * gamma: [1, 0.5, 2.1] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Gamma = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Gamma.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Gamma', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec3 uGamma;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec3 correction = (1.0 / uGamma);\n' + + 'color.r = pow(color.r, correction.r);\n' + + 'color.g = pow(color.g, correction.g);\n' + + 'color.b = pow(color.b, correction.b);\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.rgb *= color.a;\n' + + '}', + + /** + * Gamma array value, from 0.01 to 2.2. + * @param {Array} gamma + * @default + */ + gamma: [1, 1, 1], + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'gamma', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.gamma = [1, 1, 1]; + filters.BaseFilter.prototype.initialize.call(this, options); + }, + + /** + * Apply the Gamma operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, data = imageData.data, + gamma = this.gamma, len = data.length, + rInv = 1 / gamma[0], gInv = 1 / gamma[1], + bInv = 1 / gamma[2], i; + + if (!this.rVals) { + // eslint-disable-next-line + this.rVals = new Uint8Array(256); + // eslint-disable-next-line + this.gVals = new Uint8Array(256); + // eslint-disable-next-line + this.bVals = new Uint8Array(256); + } + + // This is an optimization - pre-compute a look-up table for each color channel + // instead of performing these pow calls for each pixel in the image. + for (i = 0, len = 256; i < len; i++) { + this.rVals[i] = Math.pow(i / 255, rInv) * 255; + this.gVals[i] = Math.pow(i / 255, gInv) * 255; + this.bVals[i] = Math.pow(i / 255, bInv) * 255; + } + for (i = 0, len = data.length; i < len; i += 4) { + data[i] = this.rVals[data[i]]; + data[i + 1] = this.gVals[data[i + 1]]; + data[i + 2] = this.bVals[data[i + 2]]; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uGamma: gl.getUniformLocation(program, 'uGamma'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform3fv(uniformLocations.uGamma, this.gamma); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma + */ + fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * A container class that knows how to apply a sequence of filters to an input image. + */ + filters.Composed = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Composed.prototype */ { + + type: 'Composed', + + /** + * A non sparse array of filters to apply + */ + subFilters: [], + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.subFilters = this.subFilters.slice(0); + }, + + /** + * Apply this container's filters to the input image provided. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be applied. + */ + applyTo: function(options) { + options.passes += this.subFilters.length - 1; + this.subFilters.forEach(function(filter) { + filter.applyTo(options); + }); + }, + + /** + * Serialize this filter into JSON. + * + * @returns {Object} A JSON representation of this filter. + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + subFilters: this.subFilters.map(function(filter) { return filter.toObject(); }), + }); + }, + + isNeutralState: function() { + return !this.subFilters.some(function(filter) { return !filter.isNeutralState(); }); + } + }); + + /** + * Deserialize a JSON definition of a ComposedFilter into a concrete instance. + */ + fabric.Image.filters.Composed.fromObject = function(object, callback) { + var filters = object.subFilters || [], + subFilters = filters.map(function(filter) { + return new fabric.Image.filters[filter.type](filter); + }), + instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); + callback && callback(instance); + return instance; + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * HueRotation filter class + * @class fabric.Image.filters.HueRotation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.HueRotation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.HueRotation({ + * rotation: -0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.HueRotation = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.HueRotation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'HueRotation', + + /** + * HueRotation value, from -1 to 1. + * the unit is radians + * @param {Number} myParameter + * @default + */ + rotation: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'rotation', + + calculateMatrix: function() { + var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad), + aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos; + this.matrix = [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ]; + this.matrix[0] = cos + OneMinusCos / 3; + this.matrix[1] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[2] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[5] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[6] = cos + aThird * OneMinusCos; + this.matrix[7] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[10] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[11] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[12] = cos + aThird * OneMinusCos; + }, + + /** + * HueRotation isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function(options) { + this.calculateMatrix(); + return filters.BaseFilter.prototype.isNeutralState.call(this, options); + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + this.calculateMatrix(); + filters.BaseFilter.prototype.applyTo.call(this, options); + }, + + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation + */ + fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + clone = fabric.util.object.clone; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + + var additionalProps = + ('fontFamily fontWeight fontSize text underline overline linethrough' + + ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles' + + ' direction path pathStartOffset pathSide').split(' '); + + /** + * Text class + * @class fabric.Text + * @extends fabric.Object + * @return {fabric.Text} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} + * @see {@link fabric.Text#initialize} for constructor definition + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { + + /** + * Properties which when set cause object to change dimensions + * @type Array + * @private + */ + _dimensionAffectingProps: [ + 'fontSize', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'text', + 'charSpacing', + 'textAlign', + 'styles', + 'path', + 'pathStartOffset', + 'pathSide' + ], + + /** + * @private + */ + _reNewline: /\r?\n/, + + /** + * Use this regular expression to filter for whitespaces that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpacesAndTabs: /[ \t\r]/g, + + /** + * Use this regular expression to filter for whitespace that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpaceAndTab: /[ \t\r]/, + + /** + * Use this regular expression to filter consecutive groups of non spaces. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reWords: /\S+/g, + + /** + * Type of an object + * @type String + * @default + */ + type: 'text', + + /** + * Font size (in pixels) + * @type Number + * @default + */ + fontSize: 40, + + /** + * Font weight (e.g. bold, normal, 400, 600, 800) + * @type {(Number|String)} + * @default + */ + fontWeight: 'normal', + + /** + * Font family + * @type String + * @default + */ + fontFamily: 'Times New Roman', + + /** + * Text decoration underline. + * @type Boolean + * @default + */ + underline: false, + + /** + * Text decoration overline. + * @type Boolean + * @default + */ + overline: false, + + /** + * Text decoration linethrough. + * @type Boolean + * @default + */ + linethrough: false, + + /** + * Text alignment. Possible values: "left", "center", "right", "justify", + * "justify-left", "justify-center" or "justify-right". + * @type String + * @default + */ + textAlign: 'left', + + /** + * Font style . Possible values: "", "normal", "italic" or "oblique". + * @type String + * @default + */ + fontStyle: 'normal', + + /** + * Line height + * @type Number + * @default + */ + lineHeight: 1.16, + + /** + * Superscript schema object (minimum overlap) + * @type {Object} + * @default + */ + superscript: { + size: 0.60, // fontSize factor + baseline: -0.35 // baseline-shift factor (upwards) + }, + + /** + * Subscript schema object (minimum overlap) + * @type {Object} + * @default + */ + subscript: { + size: 0.60, // fontSize factor + baseline: 0.11 // baseline-shift factor (downwards) + }, + + /** + * Background color of text lines + * @type String + * @default + */ + textBackgroundColor: '', + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps), + + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps), + + /** + * When defined, an object is rendered via stroke and this property specifies its color. + * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 + * @type String + * @default + */ + stroke: null, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * fabric.Path that the text should follow. + * since 4.6.0 the path will be drawn automatically. + * if you want to make the path visible, give it a stroke and strokeWidth or fill value + * if you want it to be hidden, assign visible = false to the path. + * This feature is in BETA, and SVG import/export is not yet supported. + * @type fabric.Path + * @example + * var textPath = new fabric.Text('Text on a path', { + * top: 150, + * left: 150, + * textAlign: 'center', + * charSpacing: -50, + * path: new fabric.Path('M 0 0 C 50 -100 150 -100 200 0', { + * strokeWidth: 1, + * visible: false + * }), + * pathSide: 'left', + * pathStartOffset: 0 + * }); + * @default + */ + path: null, + + /** + * Offset amount for text path starting position + * Only used when text has a path + * @type Number + * @default + */ + pathStartOffset: 0, + + /** + * Which side of the path the text should be drawn on. + * Only used when text has a path + * @type {String} 'left|right' + * @default + */ + pathSide: 'left', + + /** + * @private + */ + _fontSizeFraction: 0.222, + + /** + * @private + */ + offsets: { + underline: 0.10, + linethrough: -0.315, + overline: -0.88 + }, + + /** + * Text Line proportion to font Size (in pixels) + * @type Number + * @default + */ + _fontSizeMult: 1.13, + + /** + * additional space between characters + * expressed in thousands of em unit + * @type Number + * @default + */ + charSpacing: 0, + + /** + * Object containing character styles - top-level properties -> line numbers, + * 2nd-level properties - character numbers + * @type Object + * @default + */ + styles: null, + + /** + * Reference to a context to measure text char or couple of chars + * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas + * once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every + * text object created. + * @type {CanvasRenderingContext2D} + * @default + */ + _measuringContext: null, + + /** + * Baseline shift, styles only, keep at 0 for the main text object + * @type {Number} + * @default + */ + deltaY: 0, + + /** + * WARNING: EXPERIMENTAL. NOT SUPPORTED YET + * determine the direction of the text. + * This has to be set manually together with textAlign and originX for proper + * experience. + * some interesting link for the future + * https://www.w3.org/International/questions/qa-bidi-unicode-controls + * @since 4.5.0 + * @type {String} 'ltr|rtl' + * @default + */ + direction: 'ltr', + + /** + * Array of properties that define a style unit (of 'styles'). + * @type {Array} + * @default + */ + _styleProperties: [ + 'stroke', + 'strokeWidth', + 'fill', + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'underline', + 'overline', + 'linethrough', + 'deltaY', + 'textBackgroundColor', + ], + + /** + * contains characters bounding boxes + */ + __charBounds: [], + + /** + * use this size when measuring text. To avoid IE11 rounding errors + * @type {Number} + * @default + * @readonly + * @private + */ + CACHE_FONT_SIZE: 400, + + /** + * contains the min text width to avoid getting 0 + * @type {Number} + * @default + */ + MIN_TEXT_WIDTH: 2, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.text = text; + this.__skipDimension = true; + this.callSuper('initialize', options); + if (this.path) { + this.setPathInfo(); + } + this.__skipDimension = false; + this.initDimensions(); + this.setCoords(); + this.setupState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * If text has a path, it will add the extra information needed + * for path and text calculations + * @return {fabric.Text} thisArg + */ + setPathInfo: function() { + var path = this.path; + if (path) { + path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path); + } + }, + + /** + * Return a context for measurement of text string. + * if created it gets stored for reuse + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + getMeasuringContext: function() { + // if we did not return we have to measure something. + if (!fabric._measuringContext) { + fabric._measuringContext = this.canvas && this.canvas.contextCache || + fabric.util.createCanvasElement().getContext('2d'); + } + return fabric._measuringContext; + }, + + /** + * @private + * Divides text into lines of text and lines of graphemes. + */ + _splitText: function() { + var newLines = this._splitTextIntoLines(this.text); + this.textLines = newLines.lines; + this._textLines = newLines.graphemeLines; + this._unwrappedTextLines = newLines._unwrappedLines; + this._text = newLines.graphemeText; + return newLines; + }, + + /** + * Initialize or update text dimensions. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this._splitText(); + this._clearCache(); + if (this.path) { + this.width = this.path.width; + this.height = this.path.height; + } + else { + this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; + this.height = this.calcTextHeight(); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Enlarge space boxes and shift the others + */ + enlargeSpaces: function() { + var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { + continue; + } + accumulatedSpace = 0; + line = this._textLines[i]; + currentLineWidth = this.getLineWidth(i); + if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { + numberOfSpaces = spaces.length; + diffSpace = (this.width - currentLineWidth) / numberOfSpaces; + for (var j = 0, jlen = line.length; j <= jlen; j++) { + charBound = this.__charBounds[i][j]; + if (this._reSpaceAndTab.test(line[j])) { + charBound.width += diffSpace; + charBound.kernedWidth += diffSpace; + charBound.left += accumulatedSpace; + accumulatedSpace += diffSpace; + } + else { + charBound.left += accumulatedSpace; + } + } + } + } + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + return lineIndex === this._textLines.length - 1; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * It return always for text and Itext. + * @return Number + */ + missingNewlineOffset: function() { + return 1; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @param {Object} dim.x width of object to be cached + * @param {Object} dim.y height of object to be cached + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var dims = this.callSuper('_getCacheCanvasDimensions'); + var fontSize = this.fontSize; + dims.width += fontSize * dims.zoomX; + dims.height += fontSize * dims.zoomY; + return dims; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var path = this.path; + path && !path.isNotVisible() && path._render(ctx); + this._setTextStyles(ctx); + this._renderTextLinesBackground(ctx); + this._renderTextDecoration(ctx, 'underline'); + this._renderText(ctx); + this._renderTextDecoration(ctx, 'overline'); + this._renderTextDecoration(ctx, 'linethrough'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderTextStroke(ctx); + this._renderTextFill(ctx); + } + else { + this._renderTextFill(ctx); + this._renderTextStroke(ctx); + } + }, + + /** + * Set the font parameter of the context with the object properties or with charStyle + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [charStyle] object with font style properties + * @param {String} [charStyle.fontFamily] Font Family + * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix ) + * @param {String} [charStyle.fontWeight] Font weight + * @param {String} [charStyle.fontStyle] Font style (italic|normal) + */ + _setTextStyles: function(ctx, charStyle, forMeasuring) { + ctx.textBaseline = 'alphabetic'; + ctx.font = this._getFontDeclaration(charStyle, forMeasuring); + }, + + /** + * calculate and return the text Width measuring each line. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {Number} Maximum width of fabric.Text object + */ + calcTextWidth: function() { + var maxWidth = this.getLineWidth(0); + + for (var i = 1, len = this._textLines.length; i < len; i++) { + var currentLineWidth = this.getLineWidth(i); + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @param {String} method Method name ("fillText" or "strokeText") + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Text to render + * @param {Number} left Left position of text + * @param {Number} top Top position of text + * @param {Number} lineIndex Index of a line in a text + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + this._renderChars(method, ctx, line, left, top, lineIndex); + }, + + /** + * Renders the text background for lines, taking care of style + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextLinesBackground: function(ctx) { + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) { + return; + } + var heightOfLine, + lineLeftOffset, originalFill = ctx.fillStyle, + line, lastColor, + leftOffset = this._getLeftOffset(), + lineTopOffset = this._getTopOffset(), + boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path, + drawStart; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) { + lineTopOffset += heightOfLine; + continue; + } + line = this._textLines[i]; + lineLeftOffset = this._getLineLeftOffset(i); + boxWidth = 0; + boxStart = 0; + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillStyle = currentColor; + currentColor && ctx.fillRect( + -charBox.width / 2, + -heightOfLine / this.lineHeight * (1 - this._fontSizeFraction), + charBox.width, + heightOfLine / this.lineHeight + ); + ctx.restore(); + } + else if (currentColor !== lastColor) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = lastColor; + lastColor && ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + if (currentColor && !path) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentColor; + ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + } + lineTopOffset += heightOfLine; + } + ctx.fillStyle = originalFill; + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {Object} decl style declaration for cache + * @param {String} decl.fontFamily fontFamily + * @param {String} decl.fontStyle fontStyle + * @param {String} decl.fontWeight fontWeight + * @return {Object} reference to cache + */ + getFontCache: function(decl) { + var fontFamily = decl.fontFamily.toLowerCase(); + if (!fabric.charWidthsCache[fontFamily]) { + fabric.charWidthsCache[fontFamily] = { }; + } + var cache = fabric.charWidthsCache[fontFamily], + cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase(); + if (!cache[cacheProp]) { + cache[cacheProp] = { }; + } + return cache[cacheProp]; + }, + + /** + * measure and return the width of a single character. + * possibly overridden to accommodate different measure logic or + * to hook some external lib for character measurement + * @private + * @param {String} _char, char to be measured + * @param {Object} charStyle style of char to be measured + * @param {String} [previousChar] previous char + * @param {Object} [prevCharStyle] style of previous char + */ + _measureChar: function(_char, charStyle, previousChar, prevCharStyle) { + // first i try to return from cache + var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle), + previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char, + stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth, + fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth; + + if (previousChar && fontCache[previousChar] !== undefined) { + previousWidth = fontCache[previousChar]; + } + if (fontCache[_char] !== undefined) { + kernedWidth = width = fontCache[_char]; + } + if (stylesAreEqual && fontCache[couple] !== undefined) { + coupleWidth = fontCache[couple]; + kernedWidth = coupleWidth - previousWidth; + } + if (width === undefined || previousWidth === undefined || coupleWidth === undefined) { + var ctx = this.getMeasuringContext(); + // send a TRUE to specify measuring font size CACHE_FONT_SIZE + this._setTextStyles(ctx, charStyle, true); + } + if (width === undefined) { + kernedWidth = width = ctx.measureText(_char).width; + fontCache[_char] = width; + } + if (previousWidth === undefined && stylesAreEqual && previousChar) { + previousWidth = ctx.measureText(previousChar).width; + fontCache[previousChar] = previousWidth; + } + if (stylesAreEqual && coupleWidth === undefined) { + // we can measure the kerning couple and subtract the width of the previous character + coupleWidth = ctx.measureText(couple).width; + fontCache[couple] = coupleWidth; + kernedWidth = coupleWidth - previousWidth; + } + return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier }; + }, + + /** + * Computes height of character at given position + * @param {Number} line the line index number + * @param {Number} _char the character index number + * @return {Number} fontSize of the character + */ + getHeightOfChar: function(line, _char) { + return this.getValueOfPropertyAt(line, _char, 'fontSize'); + }, + + /** + * measure a text line measuring all characters. + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + measureLine: function(lineIndex) { + var lineInfo = this._measureLine(lineIndex); + if (this.charSpacing !== 0) { + lineInfo.width -= this._getWidthOfCharSpacing(); + } + if (lineInfo.width < 0) { + lineInfo.width = 0; + } + return lineInfo; + }, + + /** + * measure every grapheme of a line, populating __charBounds + * @param {Number} lineIndex + * @return {Object} object.width total width of characters + * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs + */ + _measureLine: function(lineIndex) { + var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, + graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length), + positionInPath = 0, startingPoint, totalPathLength, path = this.path, + reverse = this.pathSide === 'right'; + + this.__charBounds[lineIndex] = lineBounds; + for (i = 0; i < line.length; i++) { + grapheme = line[i]; + graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); + lineBounds[i] = graphemeInfo; + width += graphemeInfo.kernedWidth; + prevGrapheme = grapheme; + } + // this latest bound box represent the last character of the line + // to simplify cursor handling in interactive mode. + lineBounds[i] = { + left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, + width: 0, + kernedWidth: 0, + height: this.fontSize + }; + if (path) { + totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length; + startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); + startingPoint.x += path.pathOffset.x; + startingPoint.y += path.pathOffset.y; + switch (this.textAlign) { + case 'left': + positionInPath = reverse ? (totalPathLength - width) : 0; + break; + case 'center': + positionInPath = (totalPathLength - width) / 2; + break; + case 'right': + positionInPath = reverse ? 0 : (totalPathLength - width); + break; + //todo - add support for justify + } + positionInPath += this.pathStartOffset * (reverse ? -1 : 1); + for (i = reverse ? line.length - 1 : 0; + reverse ? i >= 0 : i < line.length; + reverse ? i-- : i++) { + graphemeInfo = lineBounds[i]; + if (positionInPath > totalPathLength) { + positionInPath %= totalPathLength; + } + else if (positionInPath < 0) { + positionInPath += totalPathLength; + } + // it would probably much faster to send all the grapheme position for a line + // and calculate path position/angle at once. + this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint); + positionInPath += graphemeInfo.kernedWidth; + } + } + return { width: width, numOfSpaces: numOfSpaces }; + }, + + /** + * Calculate the angle and the left,top position of the char that follow a path. + * It appends it to graphemeInfo to be reused later at rendering + * @private + * @param {Number} positionInPath to be measured + * @param {Object} graphemeInfo current grapheme box information + * @param {Object} startingPoint position of the point + */ + _setGraphemeOnPath: function(positionInPath, graphemeInfo, startingPoint) { + var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2, + path = this.path; + + // we are at currentPositionOnPath. we want to know what point on the path is. + var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo); + graphemeInfo.renderLeft = info.x - startingPoint.x; + graphemeInfo.renderTop = info.y - startingPoint.y; + graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); + }, + + /** + * Measure and return the info of a single grapheme. + * needs the the info of previous graphemes already filled + * @private + * @param {String} grapheme to be measured + * @param {Number} lineIndex index of the line where the char is + * @param {Number} charIndex position in the line + * @param {String} [prevGrapheme] character preceding the one to be measured + */ + _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { + var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), + prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { }, + info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), + kernedWidth = info.kernedWidth, + width = info.width, charSpacing; + + if (this.charSpacing !== 0) { + charSpacing = this._getWidthOfCharSpacing(); + width += charSpacing; + kernedWidth += charSpacing; + } + + var box = { + width: width, + left: 0, + height: style.fontSize, + kernedWidth: kernedWidth, + deltaY: style.deltaY, + }; + if (charIndex > 0 && !skipLeft) { + var previousBox = this.__charBounds[lineIndex][charIndex - 1]; + box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; + } + return box; + }, + + /** + * Calculate height of line at 'lineIndex' + * @param {Number} lineIndex index of line to calculate + * @return {Number} + */ + getHeightOfLine: function(lineIndex) { + if (this.__lineHeights[lineIndex]) { + return this.__lineHeights[lineIndex]; + } + + var line = this._textLines[lineIndex], + // char 0 is measured before the line cycle because it nneds to char + // emptylines + maxHeight = this.getHeightOfChar(lineIndex, 0); + for (var i = 1, len = line.length; i < len; i++) { + maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); + } + + return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; + }, + + /** + * Calculate text box height + */ + calcTextHeight: function() { + var lineHeight, height = 0; + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineHeight = this.getHeightOfLine(i); + height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); + } + return height; + }, + + /** + * @private + * @return {Number} Left offset + */ + _getLeftOffset: function() { + return this.direction === 'ltr' ? -this.width / 2 : this.width / 2; + }, + + /** + * @private + * @return {Number} Top offset + */ + _getTopOffset: function() { + return -this.height / 2; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} method Method name ("fillText" or "strokeText") + */ + _renderTextCommon: function(ctx, method) { + ctx.save(); + var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); + for (var i = 0, len = this._textLines.length; i < len; i++) { + var heightOfLine = this.getHeightOfLine(i), + maxHeight = heightOfLine / this.lineHeight, + leftOffset = this._getLineLeftOffset(i); + this._renderTextLine( + method, + ctx, + this._textLines[i], + left + leftOffset, + top + lineHeights + maxHeight, + i + ); + lineHeights += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextFill: function(ctx) { + if (!this.fill && !this.styleHas('fill')) { + return; + } + + this._renderTextCommon(ctx, 'fillText'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextStroke: function(ctx) { + if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + this._setLineDash(ctx, this.strokeDashArray); + ctx.beginPath(); + this._renderTextCommon(ctx, 'strokeText'); + ctx.closePath(); + ctx.restore(); + }, + + /** + * @private + * @param {String} method fillText or strokeText. + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} line Content of the line, splitted in an array by grapheme + * @param {Number} left + * @param {Number} top + * @param {Number} lineIndex + */ + _renderChars: function(method, ctx, line, left, top, lineIndex) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, + boxWidth = 0, + timeToRender, + path = this.path, + shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, + isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, + drawingLeft; + + ctx.save(); + top -= lineHeight * this._fontSizeFraction / this.lineHeight; + if (shortCut) { + // render all the line in one pass without checking + // drawingLeft = isLtr ? left : left - this.getLineWidth(lineIndex); + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight); + ctx.restore(); + return; + } + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing || path; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + left += sign * (charBox.kernedWidth - charBox.width); + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = this._hasStyleChanged(actualStyle, nextStyle); + } + if (timeToRender) { + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight); + ctx.restore(); + } + else { + drawingLeft = left; + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top, lineHeight); + } + charsToRender = ''; + actualStyle = nextStyle; + left += sign * boxWidth; + boxWidth = 0; + } + } + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {fabric.Gradient} filler a fabric gradient instance + * @return {CanvasPattern} a pattern to use as fill/stroke style + */ + _applyPatternGradientTransformText: function(filler) { + var pCanvas = fabric.util.createCanvasElement(), pCtx, + // TODO: verify compatibility with strokeUniform + width = this.width + this.strokeWidth, height = this.height + this.strokeWidth; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.fillStyle = filler.toLive(pCtx); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fill(); + return pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + handleFiller: function(ctx, property, filler) { + var offsetX, offsetY; + if (filler.toLive) { + if (filler.gradientUnits === 'percentage' || filler.gradientTransform || filler.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + offsetX = -this.width / 2; + offsetY = -this.height / 2; + ctx.translate(offsetX, offsetY); + ctx[property] = this._applyPatternGradientTransformText(filler); + return { offsetX: offsetX, offsetY: offsetY }; + } + else { + // is a simple gradient or pattern + ctx[property] = filler.toLive(ctx, this); + return this._applyPatternGradientTransform(ctx, filler); + } + } + else { + // is a color + ctx[property] = filler; + } + return { offsetX: 0, offsetY: 0 }; + }, + + _setStrokeStyles: function(ctx, decl) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = this.strokeLineCap; + ctx.lineDashOffset = this.strokeDashOffset; + ctx.lineJoin = this.strokeLineJoin; + ctx.miterLimit = this.strokeMiterLimit; + return this.handleFiller(ctx, 'strokeStyle', decl.stroke); + }, + + _setFillStyles: function(ctx, decl) { + return this.handleFiller(ctx, 'fillStyle', decl.fill); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {String} _char + * @param {Number} left Left coordinate + * @param {Number} top Top coordinate + * @param {Number} lineHeight Height of the line + */ + _renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) { + var decl = this._getStyleDeclaration(lineIndex, charIndex), + fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), + shouldFill = method === 'fillText' && fullDecl.fill, + shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth, + fillOffsets, strokeOffsets; + + if (!shouldStroke && !shouldFill) { + return; + } + ctx.save(); + + shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl)); + shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl)); + + ctx.font = this._getFontDeclaration(fullDecl); + + + if (decl && decl.textBackgroundColor) { + this._removeShadow(ctx); + } + if (decl && decl.deltaY) { + top += decl.deltaY; + } + shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY); + shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY); + ctx.restore(); + }, + + /** + * Turns the character into a 'superior figure' (i.e. 'superscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSuperscript: function(start, end) { + return this._setScript(start, end, this.superscript); + }, + + /** + * Turns the character into an 'inferior figure' (i.e. 'subscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSubscript: function(start, end) { + return this._setScript(start, end, this.subscript); + }, + + /** + * Applies 'schema' at given position + * @private + * @param {Number} start selection start + * @param {Number} end selection end + * @param {Number} schema + * @returns {fabric.Text} thisArg + * @chainable + */ + _setScript: function(start, end, schema) { + var loc = this.get2DCursorLocation(start, true), + fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'), + dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'), + style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline }; + this.setSelectionStyles(style, start, end); + return this; + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChanged: function(prevStyle, thisStyle) { + return prevStyle.fill !== thisStyle.fill || + prevStyle.stroke !== thisStyle.stroke || + prevStyle.strokeWidth !== thisStyle.strokeWidth || + prevStyle.fontSize !== thisStyle.fontSize || + prevStyle.fontFamily !== thisStyle.fontFamily || + prevStyle.fontWeight !== thisStyle.fontWeight || + prevStyle.fontStyle !== thisStyle.fontStyle || + prevStyle.deltaY !== thisStyle.deltaY; + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChangedForSvg: function(prevStyle, thisStyle) { + return this._hasStyleChanged(prevStyle, thisStyle) || + prevStyle.overline !== thisStyle.overline || + prevStyle.underline !== thisStyle.underline || + prevStyle.linethrough !== thisStyle.linethrough; + }, + + /** + * @private + * @param {Number} lineIndex index text line + * @return {Number} Line left offset + */ + _getLineLeftOffset: function(lineIndex) { + var lineWidth = this.getLineWidth(lineIndex), + lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, + isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); + if (textAlign === 'justify' + || (textAlign === 'justify-center' && !isEndOfWrapping) + || (textAlign === 'justify-right' && !isEndOfWrapping) + || (textAlign === 'justify-left' && !isEndOfWrapping) + ) { + return 0; + } + if (textAlign === 'center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'right') { + leftOffset = lineDiff; + } + if (textAlign === 'justify-center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'justify-right') { + leftOffset = lineDiff; + } + if (direction === 'rtl') { + leftOffset -= lineDiff; + } + return leftOffset; + }, + + /** + * @private + */ + _clearCache: function() { + this.__lineWidths = []; + this.__lineHeights = []; + this.__charBounds = []; + }, + + /** + * @private + */ + _shouldClearDimensionCache: function() { + var shouldClear = this._forceClearCache; + shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); + if (shouldClear) { + this.dirty = true; + this._forceClearCache = false; + } + return shouldClear; + }, + + /** + * Measure a single line given its index. Used to calculate the initial + * text bounding box. The values are calculated and stored in __lineWidths cache. + * @private + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + getLineWidth: function(lineIndex) { + if (this.__lineWidths[lineIndex]) { + return this.__lineWidths[lineIndex]; + } + + var width, line = this._textLines[lineIndex], lineInfo; + + if (line === '') { + width = 0; + } + else { + lineInfo = this.measureLine(lineIndex); + width = lineInfo.width; + } + this.__lineWidths[lineIndex] = width; + return width; + }, + + _getWidthOfCharSpacing: function() { + if (this.charSpacing !== 0) { + return this.fontSize * this.charSpacing / 1000; + } + return 0; + }, + + /** + * Retrieves the value of property at given character position + * @param {Number} lineIndex the line number + * @param {Number} charIndex the character number + * @param {String} property the property name + * @returns the value of 'property' + */ + getValueOfPropertyAt: function(lineIndex, charIndex, property) { + var charStyle = this._getStyleDeclaration(lineIndex, charIndex); + if (charStyle && typeof charStyle[property] !== 'undefined') { + return charStyle[property]; + } + return this[property]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextDecoration: function(ctx, type) { + if (!this[type] && !this.styleHas(type)) { + return; + } + var heightOfLine, size, _size, + lineLeftOffset, dy, _dy, + line, lastDecoration, + leftOffset = this._getLeftOffset(), + topOffset = this._getTopOffset(), top, + boxStart, boxWidth, charBox, currentDecoration, + maxHeight, currentFill, lastFill, path = this.path, + charSpacing = this._getWidthOfCharSpacing(), + offsetY = this.offsets[type]; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this[type] && !this.styleHas(type, i)) { + topOffset += heightOfLine; + continue; + } + line = this._textLines[i]; + maxHeight = heightOfLine / this.lineHeight; + lineLeftOffset = this._getLineLeftOffset(i); + boxStart = 0; + boxWidth = 0; + lastDecoration = this.getValueOfPropertyAt(i, 0, type); + lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); + top = topOffset + maxHeight * (1 - this._fontSizeFraction); + size = this.getHeightOfChar(i, 0); + dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentDecoration = this.getValueOfPropertyAt(i, j, type); + currentFill = this.getValueOfPropertyAt(i, j, 'fill'); + _size = this.getHeightOfChar(i, j); + _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); + if (path && currentDecoration && currentFill) { + ctx.save(); + ctx.fillStyle = lastFill; + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillRect( + -charBox.kernedWidth / 2, + offsetY * _size + _dy, + charBox.kernedWidth, + this.fontSize / 15 + ); + ctx.restore(); + } + else if ( + (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) + && boxWidth > 0 + ) { + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + if (lastDecoration && lastFill) { + ctx.fillStyle = lastFill; + ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth, + this.fontSize / 15 + ); + } + boxStart = charBox.left; + boxWidth = charBox.width; + lastDecoration = currentDecoration; + lastFill = currentFill; + size = _size; + dy = _dy; + } + else { + boxWidth += charBox.kernedWidth; + } + } + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentFill; + currentDecoration && currentFill && ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth - charSpacing, + this.fontSize / 15 + ); + topOffset += heightOfLine; + } + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * return font declaration string for canvas context + * @param {Object} [styleObject] object + * @returns {String} font declaration formatted for canvas context. + */ + _getFontDeclaration: function(styleObject, forMeasuring) { + var style = styleObject || this, family = this.fontFamily, + fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1; + var fontFamily = family === undefined || + family.indexOf('\'') > -1 || family.indexOf(',') > -1 || + family.indexOf('"') > -1 || fontIsGeneric + ? style.fontFamily : '"' + style.fontFamily + '"'; + return [ + // node-canvas needs "weight style", while browsers need "style weight" + // verify if this can be fixed in JSDOM + (fabric.isLikelyNode ? style.fontWeight : style.fontStyle), + (fabric.isLikelyNode ? style.fontStyle : style.fontWeight), + forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px', + fontFamily + ].join(' '); + }, + + /** + * Renders text instance on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if object is not visible + if (!this.visible) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + } + this.callSuper('render', ctx); + }, + + /** + * Returns the text as an array of lines. + * @param {String} text text to split + * @returns {Array} Lines in the text + */ + _splitTextIntoLines: function(text) { + var lines = text.split(this._reNewline), + newLines = new Array(lines.length), + newLine = ['\n'], + newText = []; + for (var i = 0; i < lines.length; i++) { + newLines[i] = fabric.util.string.graphemeSplit(lines[i]); + newText = newText.concat(newLines[i], newLine); + } + newText.pop(); + return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines }; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var allProperties = additionalProps.concat(propertiesToInclude); + var obj = this.callSuper('toObject', allProperties); + // styles will be overridden with a properly cloned structure + obj.styles = clone(this.styles, true); + if (obj.path) { + obj.path = this.path.toObject(); + } + return obj; + }, + + /** + * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. + * @param {String|Object} key Property name or object (if object, iterate over the object properties) + * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) + * @return {fabric.Object} thisArg + * @chainable + */ + set: function(key, value) { + this.callSuper('set', key, value); + var needsDims = false; + var isAddingPath = false; + if (typeof key === 'object') { + for (var _key in key) { + if (_key === 'path') { + this.setPathInfo(); + } + needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1; + isAddingPath = isAddingPath || _key === 'path'; + } + } + else { + needsDims = this._dimensionAffectingProps.indexOf(key) !== -1; + isAddingPath = key === 'path'; + } + if (isAddingPath) { + this.setPathInfo(); + } + if (needsDims) { + this.initDimensions(); + this.setCoords(); + } + return this; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) + * @static + * @memberOf fabric.Text + * @see: http://www.w3.org/TR/SVG/text.html#TextElement + */ + fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( + 'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' ')); + + /** + * Default SVG font size + * @static + * @memberOf fabric.Text + */ + fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; + + /** + * Returns fabric.Text instance from an SVG element (not yet implemented) + * @static + * @memberOf fabric.Text + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Text.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES), + parsedAnchor = parsedAttributes.textAnchor || 'left'; + options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes); + + options.top = options.top || 0; + options.left = options.left || 0; + if (parsedAttributes.textDecoration) { + var textDecoration = parsedAttributes.textDecoration; + if (textDecoration.indexOf('underline') !== -1) { + options.underline = true; + } + if (textDecoration.indexOf('overline') !== -1) { + options.overline = true; + } + if (textDecoration.indexOf('line-through') !== -1) { + options.linethrough = true; + } + delete options.textDecoration; + } + if ('dx' in parsedAttributes) { + options.left += parsedAttributes.dx; + } + if ('dy' in parsedAttributes) { + options.top += parsedAttributes.dy; + } + if (!('fontSize' in options)) { + options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + + var textContent = ''; + + // The XML is not properly parsed in IE9 so a workaround to get + // textContent is through firstChild.data. Another workaround would be + // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) + if (!('textContent' in element)) { + if ('firstChild' in element && element.firstChild !== null) { + if ('data' in element.firstChild && element.firstChild.data !== null) { + textContent = element.firstChild.data; + } + } + } + else { + textContent = element.textContent; + } + + textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); + var originalStrokeWidth = options.strokeWidth; + options.strokeWidth = 0; + + var text = new fabric.Text(textContent, options), + textHeightScaleFactor = text.getScaledHeight() / text.height, + lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, + scaledDiff = lineHeightDiff * textHeightScaleFactor, + textHeight = text.getScaledHeight() + scaledDiff, + offX = 0; + /* + Adjust positioning: + x/y attributes in SVG correspond to the bottom-left corner of text bounding box + fabric output by default at top, left. + */ + if (parsedAnchor === 'center') { + offX = text.getScaledWidth() / 2; + } + if (parsedAnchor === 'right') { + offX = text.getScaledWidth(); + } + text.set({ + left: text.left - offX, + top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight, + strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1, + }); + callback(text); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Text instance from an object representation + * @static + * @memberOf fabric.Text + * @param {Object} object plain js Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created + */ + fabric.Text.fromObject = function(object, callback) { + var objectCopy = clone(object), path = object.path; + delete objectCopy.path; + return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { + if (path) { + fabric.Object._fromObject('Path', path, function(pathInstance) { + textInstance.set('path', pathInstance); + callback(textInstance); + }, 'path'); + } + else { + callback(textInstance); + } + }, 'text'); + }; + + fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text); + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return true; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Returns true if object has a style property or has it ina specified line + * This function is used to detect if a text will use a particular property or not. + * @param {String} property to check for + * @param {Number} lineIndex to check the style on + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (!this.styles || !property || property === '') { + return false; + } + if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) { + return false; + } + var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] }; + // eslint-disable-next-line + for (var p1 in obj) { + // eslint-disable-next-line + for (var p2 in obj[p1]) { + if (typeof obj[p1][p2][property] !== 'undefined') { + return true; + } + } + } + return false; + }, + + /** + * Check if characters in a text have a value for a property + * whose value matches the textbox's value for that property. If so, + * the character-level property is deleted. If the character + * has no other properties, then it is also deleted. Finally, + * if the line containing that character has no other characters + * then it also is deleted. + * + * @param {string} property The property to compare between characters and text. + */ + cleanStyle: function(property) { + if (!this.styles || !property || property === '') { + return false; + } + var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue, + allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject; + // eslint-disable-next-line + for (var p1 in obj) { + letterCount = 0; + // eslint-disable-next-line + for (var p2 in obj[p1]) { + var styleObject = obj[p1][p2], + stylePropertyHasBeenSet = styleObject.hasOwnProperty(property); + + stylesCount++; + + if (stylePropertyHasBeenSet) { + if (!stylePropertyValue) { + stylePropertyValue = styleObject[property]; + } + else if (styleObject[property] !== stylePropertyValue) { + allStyleObjectPropertiesMatch = false; + } + + if (styleObject[property] === this[property]) { + delete styleObject[property]; + } + } + else { + allStyleObjectPropertiesMatch = false; + } + + if (Object.keys(styleObject).length !== 0) { + letterCount++; + } + else { + delete obj[p1][p2]; + } + } + + if (letterCount === 0) { + delete obj[p1]; + } + } + // if every grapheme has the same style set then + // delete those styles and set it on the parent + for (var i = 0; i < this._textLines.length; i++) { + graphemeCount += this._textLines[i].length; + } + if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) { + this[property] = stylePropertyValue; + this.removeStyle(property); + } + }, + + /** + * Remove a style property or properties from all individual character styles + * in a text object. Deletes the character style object if it contains no other style + * props. Deletes a line style object if it contains no other character styles. + * + * @param {String} props The property to remove from character styles. + */ + removeStyle: function(property) { + if (!this.styles || !property || property === '') { + return; + } + var obj = this.styles, line, lineNum, charNum; + for (lineNum in obj) { + line = obj[lineNum]; + for (charNum in line) { + delete line[charNum][property]; + if (Object.keys(line[charNum]).length === 0) { + delete line[charNum]; + } + } + if (Object.keys(line).length === 0) { + delete obj[lineNum]; + } + } + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this._getLineStyle(loc.lineIndex)) { + this._setLineStyle(loc.lineIndex); + } + + if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { + this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); + } + + fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. useful to manage styles. + */ + get2DCursorLocation: function(selectionStart, skipWrapping) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var lines = skipWrapping ? this._unwrappedTextLines : this._textLines, + len = lines.length; + for (var i = 0; i < len; i++) { + if (selectionStart <= lines[i].length) { + return { + lineIndex: i, + charIndex: selectionStart + }; + } + selectionStart -= lines[i].length + this.missingNewlineOffset(i); + } + return { + lineIndex: i - 1, + charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart + }; + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * if startIndex or endIndex are not provided, selectionStart or selectionEnd will be used. + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @param {Boolean} [complete] get full style or not + * @return {Array} styles an array with one, zero or more Style objects + */ + getSelectionStyles: function(startIndex, endIndex, complete) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + var styles = []; + for (var i = startIndex; i < endIndex; i++) { + styles.push(this.getStyleAtPosition(i, complete)); + } + return styles; + }, + + /** + * Gets style of a current selection/cursor position + * @param {Number} position to get styles at + * @param {Boolean} [complete] full style if true + * @return {Object} style Style object at a specified index + * @private + */ + getStyleAtPosition: function(position, complete) { + var loc = this.get2DCursorLocation(position), + style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) : + this._getStyleDeclaration(loc.lineIndex, loc.charIndex); + return style || {}; + }, + + /** + * Sets style of a current selection, if no selection exist, do not set anything. + * @param {Object} [styles] Styles object + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1 + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles, startIndex, endIndex) { + if (typeof startIndex === 'undefined') { + startIndex = this.selectionStart || 0; + } + if (typeof endIndex === 'undefined') { + endIndex = this.selectionEnd || startIndex; + } + for (var i = startIndex; i < endIndex; i++) { + this._extendStyles(i, styles); + } + /* not included in _extendStyles to avoid clearing cache more than once */ + this._forceClearCache = true; + return this; + }, + + /** + * get the reference, not a clone, of the style object for a given character + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Object} style object + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + var lineStyle = this.styles && this.styles[lineIndex]; + if (!lineStyle) { + return null; + } + return lineStyle[charIndex]; + }, + + /** + * return a new object that contains all the style property for a character + * the object returned is newly created + * @param {Number} lineIndex of the line where the character is + * @param {Number} charIndex position of the character on the line + * @return {Object} style object + */ + getCompleteStyleDeclaration: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex) || { }, + styleObject = { }, prop; + for (var i = 0; i < this._styleProperties.length; i++) { + prop = this._styleProperties[i]; + styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop]; + } + return styleObject; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + this.styles[lineIndex][charIndex] = style; + }, + + /** + * + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + delete this.styles[lineIndex][charIndex]; + }, + + /** + * @param {Number} lineIndex + * @return {Boolean} if the line exists or not + * @private + */ + _getLineStyle: function(lineIndex) { + return !!this.styles[lineIndex]; + }, + + /** + * Set the line style to an empty object so that is initialized + * @param {Number} lineIndex + * @private + */ + _setLineStyle: function(lineIndex) { + this.styles[lineIndex] = {}; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _deleteLineStyle: function(lineIndex) { + delete this.styles[lineIndex]; + } + }); +})(); + + +(function() { + + function parseDecoration(object) { + if (object.textDecoration) { + object.textDecoration.indexOf('underline') > -1 && (object.underline = true); + object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); + object.textDecoration.indexOf('overline') > -1 && (object.overline = true); + delete object.textDecoration; + } + } + + /** + * IText class (introduced in v1.4) Events are also fired with "text:" + * prefix when observing canvas. + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires changed + * @fires selection:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

    Supported key combinations:

    + *
    +   *   Move cursor:                    left, right, up, down
    +   *   Select character:               shift + left, shift + right
    +   *   Select text vertically:         shift + up, shift + down
    +   *   Move cursor by word:            alt + left, alt + right
    +   *   Select words:                   shift + alt + left, shift + alt + right
    +   *   Move cursor to line start/end:  cmd + left, cmd + right or home, end
    +   *   Select till start/end of line:  cmd + shift + left, cmd + shift + right or shift + home, shift + end
    +   *   Jump to start/end of text:      cmd + up, cmd + down
    +   *   Select till start/end of text:  cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
    +   *   Delete character:               backspace
    +   *   Delete word:                    alt + backspace
    +   *   Delete line:                    cmd + backspace
    +   *   Forward delete:                 delete
    +   *   Copy text:                      ctrl/cmd + c
    +   *   Paste text:                     ctrl/cmd + v
    +   *   Cut text:                       ctrl/cmd + x
    +   *   Select entire text:             ctrl/cmd + a
    +   *   Quit editing                    tab or esc
    +   * 
    + * + *

    Supported mouse/touch combination

    + *
    +   *   Position cursor:                click/touch
    +   *   Create selection:               click/touch & drag
    +   *   Create selection:               click & shift + click
    +   *   Select word:                    double click
    +   *   Select line:                    triple click
    +   * 
    + */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', + + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Number + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Number + * @default + */ + selectionEnd: 0, + + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', + + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, + + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, + + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of text cursor color in editing mode. + * if not set (default) will take color from the text. + * if set to a color value that fabric can understand, it will + * be used instead of the color of the text at the current position. + * @type String + * @default + */ + cursorColor: '', + + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Indicates whether internal text char widths can be cached + * @type Boolean + * @default + */ + caching: true, + + /** + * DOM container to append the hiddenTextarea. + * An alternative to attaching to the document.body. + * Useful to reduce laggish redraw of the full document.body tree and + * also with modals event capturing that won't let the textarea take focus. + * @type HTMLElement + * @default + */ + hiddenTextareaContainer: null, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + __widthOfSpace: [], + + /** + * Helps determining when the text is in composition, so that the cursor + * rendering is altered. + */ + inCompositionMode: false, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.callSuper('initialize', text, options); + this.initBehavior(); + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + index = Math.max(index, 0); + this._updateAndFire('selectionStart', index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + index = Math.min(index, this.text.length); + this._updateAndFire('selectionEnd', index); + }, + + /** + * @private + * @param {String} property 'selectionStart' or 'selectionEnd' + * @param {Number} index new position of property + */ + _updateAndFire: function(property, index) { + if (this[property] !== index) { + this._fireSelectionChanged(); + this[property] = index; + } + this._updateTextarea(); + }, + + /** + * Fires the even of selection changed + * @private + */ + _fireSelectionChanged: function() { + this.fire('selection:changed'); + this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + }, + + /** + * Initialize text dimensions. Render all text on given context + * or on a offscreen canvas to get the text width with measureText. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + * @private + */ + initDimensions: function() { + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this.callSuper('initDimensions'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + this.clearContextTop(); + this.callSuper('render', ctx); + // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor + // the correct position but not at every cursor animation. + this.cursorOffsetCache = { }; + this.renderCursorOrSelection(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + }, + + /** + * Prepare and clean the contextTop + */ + clearContextTop: function(skipRestore) { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { + return; + } + var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this.transform(ctx); + this._clearTextArea(ctx); + skipRestore || ctx.restore(); + }, + /** + * Renders cursor or selection (depending on what exists) + * it does on the contextTop. If contextTop is not available, do nothing. + */ + renderCursorOrSelection: function() { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { + return; + } + var boundaries = this._getCursorBoundaries(), + ctx = this.canvas.contextTop; + this.clearContextTop(true); + if (this.selectionStart === this.selectionEnd) { + this.renderCursor(boundaries, ctx); + } + else { + this.renderSelection(boundaries, ctx); + } + ctx.restore(); + }, + + _clearTextArea: function(ctx) { + // we add 4 pixel, to be sure to do not leave any pixel out + var width = this.width + 4, height = this.height + 4; + ctx.clearRect(-width / 2, -height / 2, width, height); + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(position) { + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + if (typeof position === 'undefined') { + position = this.selectionStart; + } + + var left = this._getLeftOffset(), + top = this._getTopOffset(), + offsets = this._getCursorBoundariesOffsets(position); + return { + left: left, + top: top, + leftOffset: offsets.left, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(position) { + if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { + return this.cursorOffsetCache; + } + var lineLeftOffset, + lineIndex, + charIndex, + topOffset = 0, + leftOffset = 0, + boundaries, + cursorPosition = this.get2DCursorLocation(position); + charIndex = cursorPosition.charIndex; + lineIndex = cursorPosition.lineIndex; + for (var i = 0; i < lineIndex; i++) { + topOffset += this.getHeightOfLine(i); + } + lineLeftOffset = this._getLineLeftOffset(lineIndex); + var bound = this.__charBounds[lineIndex][charIndex]; + bound && (leftOffset = bound.left); + if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) { + leftOffset -= this._getWidthOfCharSpacing(); + } + boundaries = { + top: topOffset, + left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), + }; + if (this.direction === 'rtl') { + boundaries.left *= -1; + } + this.cursorOffsetCache = boundaries; + return this.cursorOffsetCache; + }, + + /** + * Renders cursor + * @param {Object} boundaries + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderCursor: function(boundaries, ctx) { + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'), + multiplier = this.scaleX * this.canvas.getZoom(), + cursorWidth = this.cursorWidth / multiplier, + topOffset = boundaries.topOffset, + dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY'); + topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight + - charHeight * (1 - this._fontSizeFraction); + + if (this.inCompositionMode) { + this.renderSelection(boundaries, ctx); + } + ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill'); + ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; + ctx.fillRect( + boundaries.left + boundaries.leftOffset - cursorWidth / 2, + topOffset + boundaries.top + dy, + cursorWidth, + charHeight); + }, + + /** + * Renders text selection + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderSelection: function(boundaries, ctx) { + + var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart, + selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd, + isJustify = this.textAlign.indexOf('justify') !== -1, + start = this.get2DCursorLocation(selectionStart), + end = this.get2DCursorLocation(selectionEnd), + startLine = start.lineIndex, + endLine = end.lineIndex, + startChar = start.charIndex < 0 ? 0 : start.charIndex, + endChar = end.charIndex < 0 ? 0 : end.charIndex; + + for (var i = startLine; i <= endLine; i++) { + var lineOffset = this._getLineLeftOffset(i) || 0, + lineHeight = this.getHeightOfLine(i), + realLineHeight = 0, boxStart = 0, boxEnd = 0; + + if (i === startLine) { + boxStart = this.__charBounds[startLine][startChar].left; + } + if (i >= startLine && i < endLine) { + boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5? + } + else if (i === endLine) { + if (endChar === 0) { + boxEnd = this.__charBounds[endLine][endChar].left; + } + else { + var charSpacing = this._getWidthOfCharSpacing(); + boxEnd = this.__charBounds[endLine][endChar - 1].left + + this.__charBounds[endLine][endChar - 1].width - charSpacing; + } + } + realLineHeight = lineHeight; + if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) { + lineHeight /= this.lineHeight; + } + var drawStart = boundaries.left + lineOffset + boxStart, + drawWidth = boxEnd - boxStart, + drawHeight = lineHeight, extraTop = 0; + if (this.inCompositionMode) { + ctx.fillStyle = this.compositionColor || 'black'; + drawHeight = 1; + extraTop = lineHeight; + } + else { + ctx.fillStyle = this.selectionColor; + } + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - drawWidth; + } + ctx.fillRect( + drawStart, + boundaries.top + boundaries.topOffset + extraTop, + drawWidth, + drawHeight); + boundaries.topOffset += realLineHeight; + } + }, + + /** + * High level function to know the height of the cursor. + * the currentChar is the one that precedes the cursor + * Returns fontSize of char at the current cursor + * Unused from the library, is for the end user + * @return {Number} Character font size + */ + getCurrentCharFontSize: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize'); + }, + + /** + * High level function to know the color of the cursor. + * the currentChar is the one that precedes the cursor + * Returns color (fill) of char at the current cursor + * if the text object has a pattern or gradient for filler, it will return that. + * Unused by the library, is for the end user + * @return {String | fabric.Gradient | fabric.Pattern} Character color (fill) + */ + getCurrentCharColor: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fill'); + }, + + /** + * Returns the cursor position for the getCurrent.. functions + * @private + */ + _getCurrentCharIndex: function() { + var cursorPosition = this.get2DCursorLocation(this.selectionStart, true), + charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0; + return { l: cursorPosition.lineIndex, c: charIndex }; + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as argument + */ + fabric.IText.fromObject = function(object, callback) { + parseDecoration(object); + if (object.styles) { + for (var i in object.styles) { + for (var j in object.styles[i]) { + parseDecoration(object.styles[i][j]); + } + } + } + fabric.Object._fromObject('IText', object, callback, 'text'); + }; +})(); + + +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initAddedHandler(); + this.initRemovedHandler(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.mouseMoveHandler = this.mouseMoveHandler.bind(this); + }, + + onDeselect: function() { + this.isEditing && this.exitEditing(); + this.selected = false; + }, + + /** + * Initializes "added" event handler + */ + initAddedHandler: function() { + var _this = this; + this.on('added', function() { + var canvas = _this.canvas; + if (canvas) { + if (!canvas._hasITextHandlers) { + canvas._hasITextHandlers = true; + _this._initCanvasHandlers(canvas); + } + canvas._iTextInstances = canvas._iTextInstances || []; + canvas._iTextInstances.push(_this); + } + }); + }, + + initRemovedHandler: function() { + var _this = this; + this.on('removed', function() { + var canvas = _this.canvas; + if (canvas) { + canvas._iTextInstances = canvas._iTextInstances || []; + fabric.util.removeFromArray(canvas._iTextInstances, _this); + if (canvas._iTextInstances.length === 0) { + canvas._hasITextHandlers = false; + _this._removeCanvasHandlers(canvas); + } + } + }); + }, + + /** + * register canvas event to manage exiting on other instances + * @private + */ + _initCanvasHandlers: function(canvas) { + canvas._mouseUpITextHandler = function() { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.__isMousedown = false; + }); + } + }; + canvas.on('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * remove canvas event to manage exiting on other instances + * @private + */ + _removeCanvasHandlers: function(canvas) { + canvas.off('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * @private + */ + _tick: function() { + this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete'); + }, + + /** + * @private + */ + _animateCursor: function(obj, targetOpacity, duration, completeMethod) { + + var tickState; + + tickState = { + isAborted: false, + abort: function() { + this.isAborted = true; + }, + }; + + obj.animate('_currentCursorOpacity', targetOpacity, { + duration: duration, + onComplete: function() { + if (!tickState.isAborted) { + obj[completeMethod](); + } + }, + onChange: function() { + // we do not want to animate a selection, only cursor + if (obj.canvas && obj.selectionStart === obj.selectionEnd) { + obj.renderCursorOrSelection(); + } + }, + abort: function() { + return tickState.isAborted; + } + }); + return tickState; + }, + + /** + * @private + */ + _onTickComplete: function() { + + var _this = this; + + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick'); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function(restart) { + var _this = this, + delay = restart ? 0 : this.cursorDelay; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this._cursorTimeout2 = setTimeout(function() { + _this._tick(); + }, delay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + var shouldClear = this._currentTickState || this._currentTickCompleteState, + canvas = this.canvas; + this._currentTickState && this._currentTickState.abort(); + this._currentTickCompleteState && this._currentTickCompleteState.abort(); + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + // to clear just itext area we need to transform the context + // it may not be worth it + if (shouldClear && canvas) { + canvas.clearContext(canvas.contextTop || canvas.contextContainer); + } + + }, + + /** + * Selects entire text + * @return {fabric.IText} thisArg + * @chainable + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this._text.length; + this._fireSelectionChanged(); + this._updateTextarea(); + return this; + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this._text.slice(this.selectionStart, this.selectionEnd).join(''); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { + offset++; + index--; + } + } + while (/\S/.test(this._text[index]) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { + offset++; + index++; + } + } + while (/\S/.test(this._text[index]) && index < this._text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this._text[index]) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this._text[index]) && index < this._text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction 1 or -1 + * @return {Number} Index of the beginning or end of a word + */ + searchWordBoundary: function(selectionStart, direction) { + var text = this._text, + index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart, + _char = text[index], + // wrong + reNonWord = fabric.reNonWord; + + while (!reNonWord.test(_char) && index > 0 && index < text.length) { + index += direction; + _char = text[index]; + } + if (reNonWord.test(_char)) { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */ + newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + * @return {fabric.IText} thisArg + * @chainable + */ + selectLine: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.findLineBoundaryLeft(selectionStart), + newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + return this; + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function(e) { + if (this.isEditing || !this.editable) { + return; + } + + if (this.canvas) { + this.canvas.calcOffset(); + this.exitEditingOnOthers(this.canvas); + } + + this.isEditing = true; + + this.initHiddenTextarea(e); + this.hiddenTextarea.focus(); + this.hiddenTextarea.value = this.text; + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + this._textBeforeEdit = this.text; + + this._tick(); + this.fire('editing:entered'); + this._fireSelectionChanged(); + if (!this.canvas) { + return this; + } + this.canvas.fire('text:editing:entered', { target: this }); + this.initMouseMoveHandler(); + this.canvas.requestRenderAll(); + return this; + }, + + exitEditingOnOthers: function(canvas) { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.selected = false; + if (obj.isEditing) { + obj.exitEditing(); + } + }); + } + }, + + /** + * Initializes "mousemove" event handler + */ + initMouseMoveHandler: function() { + this.canvas.on('mouse:move', this.mouseMoveHandler); + }, + + /** + * @private + */ + mouseMoveHandler: function(options) { + if (!this.__isMousedown || !this.isEditing) { + return; + } + + var newSelectionStart = this.getSelectionStartFromPointer(options.e), + currentStart = this.selectionStart, + currentEnd = this.selectionEnd; + if ( + (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) + && + (currentStart === newSelectionStart || currentEnd === newSelectionStart) + ) { + return; + } + if (newSelectionStart > this.__selectionStartOnMouseDown) { + this.selectionStart = this.__selectionStartOnMouseDown; + this.selectionEnd = newSelectionStart; + } + else { + this.selectionStart = newSelectionStart; + this.selectionEnd = this.__selectionStartOnMouseDown; + } + if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { + this.restartCursorIfNeeded(); + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + } + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * convert from textarea to grapheme indexes + */ + fromStringToGraphemeSelection: function(start, end, text) { + var smallerTextStart = text.slice(0, start), + graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; + if (start === end) { + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + } + var smallerTextEnd = text.slice(start, end), + graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + }, + + /** + * convert from fabric to textarea values + */ + fromGraphemeToStringSelection: function(start, end, _text) { + var smallerTextStart = _text.slice(0, start), + graphemeStart = smallerTextStart.join('').length; + if (start === end) { + return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; + } + var smallerTextEnd = _text.slice(start, end), + graphemeEnd = smallerTextEnd.join('').length; + return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; + }, + + /** + * @private + */ + _updateTextarea: function() { + this.cursorOffsetCache = { }; + if (!this.hiddenTextarea) { + return; + } + if (!this.inCompositionMode) { + var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text); + this.hiddenTextarea.selectionStart = newSelection.selectionStart; + this.hiddenTextarea.selectionEnd = newSelection.selectionEnd; + } + this.updateTextareaPosition(); + }, + + /** + * @private + */ + updateFromTextArea: function() { + if (!this.hiddenTextarea) { + return; + } + this.cursorOffsetCache = { }; + this.text = this.hiddenTextarea.value; + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + var newSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value); + this.selectionEnd = this.selectionStart = newSelection.selectionEnd; + if (!this.inCompositionMode) { + this.selectionStart = newSelection.selectionStart; + } + this.updateTextareaPosition(); + }, + + /** + * @private + */ + updateTextareaPosition: function() { + if (this.selectionStart === this.selectionEnd) { + var style = this._calcTextareaPosition(); + this.hiddenTextarea.style.left = style.left; + this.hiddenTextarea.style.top = style.top; + } + }, + + /** + * @private + * @return {Object} style contains style for hiddenTextarea + */ + _calcTextareaPosition: function() { + if (!this.canvas) { + return { x: 1, y: 1 }; + } + var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart, + boundaries = this._getCursorBoundaries(desiredPosition), + cursorLocation = this.get2DCursorLocation(desiredPosition), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight, + leftOffset = boundaries.leftOffset, + m = this.calcTransformMatrix(), + p = { + x: boundaries.left + leftOffset, + y: boundaries.top + boundaries.topOffset + charHeight + }, + retinaScaling = this.canvas.getRetinaScaling(), + upperCanvas = this.canvas.upperCanvasEl, + upperCanvasWidth = upperCanvas.width / retinaScaling, + upperCanvasHeight = upperCanvas.height / retinaScaling, + maxWidth = upperCanvasWidth - charHeight, + maxHeight = upperCanvasHeight - charHeight, + scaleX = upperCanvas.clientWidth / upperCanvasWidth, + scaleY = upperCanvas.clientHeight / upperCanvasHeight; + + p = fabric.util.transformPoint(p, m); + p = fabric.util.transformPoint(p, this.canvas.viewportTransform); + p.x *= scaleX; + p.y *= scaleY; + if (p.x < 0) { + p.x = 0; + } + if (p.x > maxWidth) { + p.x = maxWidth; + } + if (p.y < 0) { + p.y = 0; + } + if (p.y > maxHeight) { + p.y = maxHeight; + } + + // add canvas offset on document + p.x += this.canvas._offset.left; + p.y += this.canvas._offset.top; + + return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight }; + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + selectable: this.selectable, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) { + return; + } + + this.hoverCursor = this._savedProps.hoverCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.selectable = this._savedProps.selectable; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + var isTextChanged = (this._textBeforeEdit !== this.text); + var hiddenTextarea = this.hiddenTextarea; + this.selected = false; + this.isEditing = false; + + this.selectionEnd = this.selectionStart; + + if (hiddenTextarea) { + hiddenTextarea.blur && hiddenTextarea.blur(); + hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea); + } + this.hiddenTextarea = null; + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this.fire('editing:exited'); + isTextChanged && this.fire('modified'); + if (this.canvas) { + this.canvas.off('mouse:move', this.mouseMoveHandler); + this.canvas.fire('text:editing:exited', { target: this }); + isTextChanged && this.canvas.fire('object:modified', { target: this }); + } + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + for (var prop in this.styles) { + if (!this._textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * remove and reflow a style block from start to end. + * @param {Number} start linear start position for removal (included in removal) + * @param {Number} end linear end position for removal ( excluded from removal ) + */ + removeStyleFromTo: function(start, end) { + var cursorStart = this.get2DCursorLocation(start, true), + cursorEnd = this.get2DCursorLocation(end, true), + lineStart = cursorStart.lineIndex, + charStart = cursorStart.charIndex, + lineEnd = cursorEnd.lineIndex, + charEnd = cursorEnd.charIndex, + i, styleObj; + if (lineStart !== lineEnd) { + // step1 remove the trailing of lineStart + if (this.styles[lineStart]) { + for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) { + delete this.styles[lineStart][i]; + } + } + // step2 move the trailing of lineEnd to lineStart if needed + if (this.styles[lineEnd]) { + for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) { + styleObj = this.styles[lineEnd][i]; + if (styleObj) { + this.styles[lineStart] || (this.styles[lineStart] = { }); + this.styles[lineStart][charStart + i - charEnd] = styleObj; + } + } + } + // step3 detects lines will be completely removed. + for (i = lineStart + 1; i <= lineEnd; i++) { + delete this.styles[i]; + } + // step4 shift remaining lines. + this.shiftLineStyles(lineEnd, lineStart - lineEnd); + } + else { + // remove and shift left on the same line + if (this.styles[lineStart]) { + styleObj = this.styles[lineStart]; + var diff = charEnd - charStart, numericChar, _char; + for (i = charStart; i < charEnd; i++) { + delete styleObj[i]; + } + for (_char in this.styles[lineStart]) { + numericChar = parseInt(_char, 10); + if (numericChar >= charEnd) { + styleObj[numericChar - diff] = styleObj[_char]; + delete styleObj[_char]; + } + } + } + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can any number? + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by offset upward or downward + // do not clone deep. we need new array, not new style objects + var clonedStyles = clone(this.styles); + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + if (!clonedStyles[numericLine - offset]) { + delete this.styles[numericLine]; + } + } + } + }, + + restartCursorIfNeeded: function() { + if (!this._currentTickState || this._currentTickState.isAborted + || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted + ) { + this.initDelayedCursor(); + } + }, + + /** + * Handle insertion of more consecutive style lines for when one or more + * newlines gets added to the text. Since current style needs to be shifted + * first we shift the current style of the number lines needed, then we add + * new lines from the last to the first. + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Number} qty number of lines to add + * @param {Array} copiedStyle Array of objects styles + */ + insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) { + var currentCharStyle, + newLineStyles = {}, + somethingAdded = false, + isEndOfLine = this._unwrappedTextLines[lineIndex].length === charIndex; + + qty || (qty = 1); + this.shiftLineStyles(lineIndex, qty); + if (this.styles[lineIndex]) { + currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1]; + } + // we clone styles of all chars + // after cursor onto the current line + for (var index in this.styles[lineIndex]) { + var numIndex = parseInt(index, 10); + if (numIndex >= charIndex) { + somethingAdded = true; + newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + if (!(isEndOfLine && charIndex === 0)) { + delete this.styles[lineIndex][index]; + } + } + } + var styleCarriedOver = false; + if (somethingAdded && !isEndOfLine) { + // if is end of line, the extra style we copied + // is probably not something we want + this.styles[lineIndex + qty] = newLineStyles; + styleCarriedOver = true; + } + if (styleCarriedOver) { + // skip the last line of since we already prepared it. + qty--; + } + // for the all the lines or all the other lines + // we clone current char style onto the next (otherwise empty) line + while (qty > 0) { + if (copiedStyle && copiedStyle[qty - 1]) { + this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty - 1]) }; + } + else if (currentCharStyle) { + this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) }; + } + else { + delete this.styles[lineIndex + qty]; + } + qty--; + } + this._forceClearCache = true; + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Number} quantity number Style object to insert, if given + * @param {Array} copiedStyle array of style objects + */ + insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) { + if (!this.styles) { + this.styles = {}; + } + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {}; + + quantity || (quantity = 1); + // shift all char styles by quantity forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex]; + // only delete the style if there was nothing moved there + if (!currentLineStylesCloned[numericIndex - quantity]) { + delete currentLineStyles[numericIndex]; + } + } + } + this._forceClearCache = true; + if (copiedStyle) { + while (quantity--) { + if (!Object.keys(copiedStyle[quantity]).length) { + continue; + } + if (!this.styles[lineIndex]) { + this.styles[lineIndex] = {}; + } + this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]); + } + return; + } + if (!currentLineStyles) { + return; + } + var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1]; + while (newStyle && quantity--) { + this.styles[lineIndex][charIndex + quantity] = clone(newStyle); + } + }, + + /** + * Inserts style object(s) + * @param {Array} insertedText Characters at the location where style is inserted + * @param {Number} start cursor index for inserting style + * @param {Array} [copiedStyle] array of style objects to insert. + */ + insertNewStyleBlock: function(insertedText, start, copiedStyle) { + var cursorLoc = this.get2DCursorLocation(start, true), + addedLines = [0], linesLength = 0; + // get an array of how many char per lines are being added. + for (var i = 0; i < insertedText.length; i++) { + if (insertedText[i] === '\n') { + linesLength++; + addedLines[linesLength] = 0; + } + else { + addedLines[linesLength]++; + } + } + // for the first line copy the style from the current char position. + if (addedLines[0] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle); + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1); + } + linesLength && this.insertNewlineStyleObject( + cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength); + for (var i = 1; i < linesLength; i++) { + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); + } + else if (copiedStyle) { + this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0]; + } + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1); + } + // we use i outside the loop to get it like linesLength + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); + } + }, + + /** + * Set the selectionStart and selectionEnd according to the new position of cursor + * mimic the key - mouse navigation when shift is pressed. + */ + setSelectionStartEndWithShift: function(start, end, newSelection) { + if (newSelection <= start) { + if (end === start) { + this._selectionDirection = 'left'; + } + else if (this._selectionDirection === 'right') { + this._selectionDirection = 'left'; + this.selectionEnd = start; + } + this.selectionStart = newSelection; + } + else if (newSelection > start && newSelection < end) { + if (this._selectionDirection === 'right') { + this.selectionEnd = newSelection; + } + else { + this.selectionStart = newSelection; + } + } + else { + // newSelection is > selection start and end + if (end === start) { + this._selectionDirection = 'right'; + } + else if (this._selectionDirection === 'left') { + this._selectionDirection = 'right'; + this.selectionStart = end; + } + this.selectionEnd = newSelection; + } + }, + + setSelectionInBoundaries: function() { + var length = this.text.length; + if (this.selectionStart > length) { + this.selectionStart = length; + } + else if (this.selectionStart < 0) { + this.selectionStart = 0; + } + if (this.selectionEnd > length) { + this.selectionEnd = length; + } + else if (this.selectionEnd < 0) { + this.selectionEnd = 0; + } + } + }); +})(); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.__lastPointer = { }; + + this.on('mousedown', this.onMouseDown); + }, + + /** + * Default event handler to simulate triple click + * @private + */ + onMouseDown: function(options) { + if (!this.canvas) { + return; + } + this.__newClickTime = +new Date(); + var newPointer = options.pointer; + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + this.__lastIsEditing = this.isEditing; + this.__lastSelected = this.selected; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + /** + * @private + */ + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, + + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initMousedownHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, + + /** + * Default handler for double click, select a word + */ + doubleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Default handler for triple click, select a line + */ + tripleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('mousedblclick', this.doubleClickHandler); + this.on('tripleclick', this.tripleClickHandler); + }, + + /** + * Default event handler for the basic functionalities needed on _mouseDown + * can be overridden to do something different. + * Scope of this implementation is: find the click position, set selectionStart + * find selectionEnd, initialize the drawing of either cursor or selection area + * initializing a mousedDown on a text area will cancel fabricjs knowledge of + * current compositionMode. It will be set to false. + */ + _mouseDownHandler: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + + this.__isMousedown = true; + + if (this.selected) { + this.inCompositionMode = false; + this.setCursorByClick(options.e); + } + + if (this.isEditing) { + this.__selectionStartOnMouseDown = this.selectionStart; + if (this.selectionStart === this.selectionEnd) { + this.abortCursorAnimation(); + } + this.renderCursorOrSelection(); + } + }, + + /** + * Default event handler for the basic functionalities needed on mousedown:before + * can be overridden to do something different. + * Scope of this implementation is: verify the object is already selected when mousing down + */ + _mouseDownHandlerBefore: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + // we want to avoid that an object that was selected and then becomes unselectable, + // may trigger editing mode in some way. + this.selected = this === this.canvas._activeObject; + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', this._mouseDownHandler); + this.on('mousedown:before', this._mouseDownHandlerBefore); + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', this.mouseUpHandler); + }, + + /** + * standard handler for mouse up, overridable + * @private + */ + mouseUpHandler: function(options) { + this.__isMousedown = false; + if (!this.editable || this.group || + (options.transform && options.transform.actionPerformed) || + (options.e.button && options.e.button !== 1)) { + return; + } + + if (this.canvas) { + var currentActive = this.canvas._activeObject; + if (currentActive && currentActive !== this) { + // avoid running this logic when there is an active object + // this because is possible with shift click and fast clicks, + // to rapidly deselect and reselect this object and trigger an enterEdit + return; + } + } + + if (this.__lastSelected && !this.__corner) { + this.selected = false; + this.__lastSelected = false; + this.enterEditing(options.e); + if (this.selectionStart === this.selectionEnd) { + this.initDelayedCursor(true); + } + else { + this.renderCursorOrSelection(); + } + } + else { + this.selected = true; + } + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Event} e Event object + */ + setCursorByClick: function(e) { + var newSelection = this.getSelectionStartFromPointer(e), + start = this.selectionStart, end = this.selectionEnd; + if (e.shiftKey) { + this.setSelectionStartEndWithShift(start, end, newSelection); + } + else { + this.selectionStart = newSelection; + this.selectionEnd = newSelection; + } + if (this.isEditing) { + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + var mouseOffset = this.getLocalPointer(e), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + lineIndex = 0, + lineLeftOffset, + line; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (height <= mouseOffset.y) { + height += this.getHeightOfLine(i) * this.scaleY; + lineIndex = i; + if (i > 0) { + charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1); + } + } + else { + break; + } + } + lineLeftOffset = this._getLineLeftOffset(lineIndex); + width = lineLeftOffset * this.scaleX; + line = this._textLines[lineIndex]; + // handling of RTL: in order to get things work correctly, + // we assume RTL writing is mirrored compared to LTR writing. + // so in position detection we mirror the X offset, and when is time + // of rendering it, we mirror it again. + if (this.direction === 'rtl') { + mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + } + for (var j = 0, jlen = line.length; j < jlen; j++) { + prevWidth = width; + // i removed something about flipX here, check. + width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX; + if (width <= mouseOffset.x) { + charIndex++; + } + else { + break; + } + } + return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen); + }, + + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0 + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor || + distanceBtwNextCharAndCursor < 0 ? 0 : 1, + newSelectionStart = index + offset; + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this._text.length) { + newSelectionStart = this._text.length; + } + + return newSelectionStart; + } +}); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + this.hiddenTextarea.setAttribute('autocorrect', 'off'); + this.hiddenTextarea.setAttribute('autocomplete', 'off'); + this.hiddenTextarea.setAttribute('spellcheck', 'false'); + this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', ''); + this.hiddenTextarea.setAttribute('wrap', 'off'); + var style = this._calcTextareaPosition(); + // line-height: 1px; was removed from the style to fix this: + // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 + this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + + ' paddingーtop: ' + style.fontSize + ';'; + + if (this.hiddenTextareaContainer) { + this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); + } + else { + fabric.document.body.appendChild(this.hiddenTextarea); + } + + fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); + + if (!this._clickHandlerInitialized && this.canvas) { + fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this)); + this._clickHandlerInitialized = true; + } + }, + + /** + * For functionalities on keyDown + * Map a special key to a function of the instance/prototype + * If you need different behaviour for ESC or TAB or arrows, you have to change + * this map setting the name of a function that you build on the fabric.Itext or + * your prototype. + * the map change will affect all Instances unless you need for only some text Instances + * in that case you have to clone this object and assign your Instance. + * this.keysMap = fabric.util.object.clone(this.keysMap); + * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0] + */ + keysMap: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorRight', + 36: 'moveCursorLeft', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + }, + + keysMapRtl: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorLeft', + 36: 'moveCursorRight', + 37: 'moveCursorRight', + 38: 'moveCursorUp', + 39: 'moveCursorLeft', + 40: 'moveCursorDown', + }, + + /** + * For functionalities on keyUp + ctrl || cmd + */ + ctrlKeysMapUp: { + 67: 'copy', + 88: 'cut' + }, + + /** + * For functionalities on keyDown + ctrl || cmd + */ + ctrlKeysMapDown: { + 65: 'selectAll' + }, + + onClick: function() { + // No need to trigger click event here, focus is enough to have the keyboard appear on Android + this.hiddenTextarea && this.hiddenTextarea.focus(); + }, + + /** + * Handles keydown event + * only used for arrows and combination of modifier keys. + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) { + return; + } + var keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap; + if (e.keyCode in keyMap) { + this[keyMap[e.keyCode]](e); + } + else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapDown[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.keyCode >= 33 && e.keyCode <= 40) { + // if i press an arrow key just update selection + this.inCompositionMode = false; + this.clearContextTop(); + this.renderCursorOrSelection(); + } + else { + this.canvas && this.canvas.requestRenderAll(); + } + }, + + /** + * Handles keyup event + * We handle KeyUp because ie11 and edge have difficulties copy/pasting + * if a copy/cut event fired, keyup is dismissed + * @param {Event} e Event object + */ + onKeyUp: function(e) { + if (!this.isEditing || this._copyDone || this.inCompositionMode) { + this._copyDone = false; + return; + } + if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapUp[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + this.canvas && this.canvas.requestRenderAll(); + }, + + /** + * Handles onInput event + * @param {Event} e Event object + */ + onInput: function(e) { + var fromPaste = this.fromPaste; + this.fromPaste = false; + e && e.stopPropagation(); + if (!this.isEditing) { + return; + } + // decisions about style changes. + var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText, + charCount = this._text.length, + nextCharCount = nextText.length, + removedText, insertedText, + charDiff = nextCharCount - charCount, + selectionStart = this.selectionStart, selectionEnd = this.selectionEnd, + selection = selectionStart !== selectionEnd, + copiedStyle, removeFrom, removeTo; + if (this.hiddenTextarea.value === '') { + this.styles = { }; + this.updateFromTextArea(); + this.fire('changed'); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.requestRenderAll(); + } + return; + } + + var textareaSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, + this.hiddenTextarea.selectionEnd, + this.hiddenTextarea.value + ); + var backDelete = selectionStart > textareaSelection.selectionStart; + + if (selection) { + removedText = this._text.slice(selectionStart, selectionEnd); + charDiff += selectionEnd - selectionStart; + } + else if (nextCharCount < charCount) { + if (backDelete) { + removedText = this._text.slice(selectionEnd + charDiff, selectionEnd); + } + else { + removedText = this._text.slice(selectionStart, selectionStart - charDiff); + } + } + insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd); + if (removedText && removedText.length) { + if (insertedText.length) { + // let's copy some style before deleting. + // we want to copy the style before the cursor OR the style at the cursor if selection + // is bigger than 0. + copiedStyle = this.getSelectionStyles(selectionStart, selectionStart + 1, false); + // now duplicate the style one for each inserted text. + copiedStyle = insertedText.map(function() { + // this return an array of references, but that is fine since we are + // copying the style later. + return copiedStyle[0]; + }); + } + if (selection) { + removeFrom = selectionStart; + removeTo = selectionEnd; + } + else if (backDelete) { + // detect differences between forwardDelete and backDelete + removeFrom = selectionEnd - removedText.length; + removeTo = selectionEnd; + } + else { + removeFrom = selectionEnd; + removeTo = selectionEnd + removedText.length; + } + this.removeStyleFromTo(removeFrom, removeTo); + } + if (insertedText.length) { + if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) { + copiedStyle = fabric.copiedTextStyle; + } + this.insertNewStyleBlock(insertedText, selectionStart, copiedStyle); + } + this.updateFromTextArea(); + this.fire('changed'); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.requestRenderAll(); + } + }, + /** + * Composition start + */ + onCompositionStart: function() { + this.inCompositionMode = true; + }, + + /** + * Composition end + */ + onCompositionEnd: function() { + this.inCompositionMode = false; + }, + + // /** + // * Composition update + // */ + onCompositionUpdate: function(e) { + this.compositionStart = e.target.selectionStart; + this.compositionEnd = e.target.selectionEnd; + this.updateTextareaPosition(); + }, + + /** + * Copies selected text + * @param {Event} e Event object + */ + copy: function() { + if (this.selectionStart === this.selectionEnd) { + //do not cut-copy if no selection + return; + } + + fabric.copiedText = this.getSelectedText(); + if (!fabric.disableStyleCopyPaste) { + fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true); + } + else { + fabric.copiedTextStyle = null; + } + this._copyDone = true; + }, + + /** + * Pastes text + * @param {Event} e Event object + */ + paste: function() { + this.fromPaste = true; + }, + + /** + * @private + * @param {Event} e Event object + * @return {Object} Clipboard data object + */ + _getClipboardData: function(e) { + return (e && e.clipboardData) || fabric.window.clipboardData; + }, + + /** + * Finds the width in pixels before the cursor on the same line + * @private + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Number} widthBeforeCursor width before cursor + */ + _getWidthBeforeCursor: function(lineIndex, charIndex) { + var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound; + + if (charIndex > 0) { + bound = this.__charBounds[lineIndex][charIndex - 1]; + widthBeforeCursor += bound.left + bound.width; + } + return widthBeforeCursor; + }, + + /** + * Gets start offset of a selection + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + // if on last line, down cursor goes to end of line + if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) { + // move to the end of a text + return this._text.length - selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor), + textAfterCursor = this._textLines[lineIndex].slice(charIndex); + return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex); + }, + + /** + * private + * Helps finding if the offset should be counted from Start or End + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + _getSelectionForOffset: function(e, isRight) { + if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) { + return this.selectionEnd; + } + else { + return this.selectionStart; + } + }, + + /** + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getUpCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + if (lineIndex === 0 || e.metaKey || e.keyCode === 33) { + // if on first line, up cursor goes to start of line + return -selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), + textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), + missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1); + // return a negative offset + return -this._textLines[lineIndex - 1].length + + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset); + }, + + /** + * for a given width it founds the matching character. + * @private + */ + _getIndexOnLine: function(lineIndex, width) { + + var line = this._textLines[lineIndex], + lineLeftOffset = this._getLineLeftOffset(lineIndex), + widthOfCharsOnLine = lineLeftOffset, + indexOnLine = 0, charWidth, foundMatch; + + for (var j = 0, jlen = line.length; j < jlen; j++) { + charWidth = this.__charBounds[lineIndex][j].width; + widthOfCharsOnLine += charWidth; + if (widthOfCharsOnLine > width) { + foundMatch = true; + var leftEdge = widthOfCharsOnLine - charWidth, + rightEdge = widthOfCharsOnLine, + offsetFromLeftEdge = Math.abs(leftEdge - width), + offsetFromRightEdge = Math.abs(rightEdge - width); + + indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + break; + } + } + + // reached end + if (!foundMatch) { + indexOnLine = line.length - 1; + } + + return indexOnLine; + }, + + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { + return; + } + this._moveCursorUpOrDown('Down', e); + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorUpOrDown('Up', e); + }, + + /** + * Moves cursor up or down, fires the events + * @param {String} direction 'Up' or 'Down' + * @param {Event} e Event object + */ + _moveCursorUpOrDown: function(direction, e) { + // getUpCursorOffset + // getDownCursorOffset + var action = 'get' + direction + 'CursorOffset', + offset = this[action](e, this._selectionDirection === 'right'); + if (e.shiftKey) { + this.moveCursorWithShift(offset); + } + else { + this.moveCursorWithoutShift(offset); + } + if (offset !== 0) { + this.setSelectionInBoundaries(); + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor with shift + * @param {Number} offset + */ + moveCursorWithShift: function(offset) { + var newSelection = this._selectionDirection === 'left' + ? this.selectionStart + offset + : this.selectionEnd + offset; + this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); + return offset !== 0; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorWithoutShift: function(offset) { + if (offset < 0) { + this.selectionStart += offset; + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += offset; + this.selectionStart = this.selectionEnd; + } + return offset !== 0; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorLeftOrRight('Left', e); + }, + + /** + * @private + * @return {Boolean} true if a change happened + */ + _move: function(e, prop, direction) { + var newValue; + if (e.altKey) { + newValue = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += direction === 'Left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + return this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + var change = true; + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) { + change = this._moveLeft(e, 'selectionStart'); + + } + this.selectionEnd = this.selectionStart; + return change; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + return this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0){ + this._selectionDirection = 'left'; + return this._moveLeft(e, 'selectionStart'); + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { + return; + } + this._moveCursorLeftOrRight('Right', e); + }, + + /** + * Moves cursor right or Left, fires event + * @param {String} direction 'Left', 'Right' + * @param {Event} e Event object + */ + _moveCursorLeftOrRight: function(direction, e) { + var actionName = 'moveCursor' + direction + 'With'; + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + actionName += 'Shift'; + } + else { + actionName += 'outShift'; + } + if (this[actionName](e)) { + this.abortCursorAnimation(); + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + return this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this._text.length) { + this._selectionDirection = 'right'; + return this._moveRight(e, 'selectionEnd'); + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e Event object + */ + moveCursorRightWithoutShift: function(e) { + var changed = true; + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + return changed; + }, + + /** + * Removes characters from start/end + * start/end ar per grapheme position in _text array. + * + * @param {Number} start + * @param {Number} end default to start + 1 + */ + removeChars: function(start, end) { + if (typeof end === 'undefined') { + end = start + 1; + } + this.removeStyleFromTo(start, end); + this._text.splice(start, end - start); + this.text = this._text.join(''); + this.set('dirty', true); + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this._removeExtraneousStyles(); + }, + + /** + * insert characters at start position, before start position. + * start equal 1 it means the text get inserted between actual grapheme 0 and 1 + * if style array is provided, it must be as the same length of text in graphemes + * if end is provided and is bigger than start, old text is replaced. + * start/end ar per grapheme position in _text array. + * + * @param {String} text text to insert + * @param {Array} style array of style objects + * @param {Number} start + * @param {Number} end default to start + 1 + */ + insertChars: function(text, style, start, end) { + if (typeof end === 'undefined') { + end = start; + } + if (end > start) { + this.removeStyleFromTo(start, end); + } + var graphemes = fabric.util.string.graphemeSplit(text); + this.insertNewStyleBlock(graphemes, start, style); + this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); + this.text = this._text.join(''); + this.set('dirty', true); + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } + this._removeExtraneousStyles(); + }, + +}); + + +/* _TO_SVG_START_ */ +(function() { + var toFixed = fabric.util.toFixed, + multipleSpacesRegex = / +/g; + + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { + + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function() { + var offsets = this._getSVGLeftTopOffsets(), + textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); + return this._wrapSVGTextAndBg(textAndBg); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + return this._createBaseSVGMarkup( + this._toSVG(), + { reviver: reviver, noStyle: true, withShadow: true } + ); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function() { + return { + textLeft: -this.width / 2, + textTop: -this.height / 2, + lineTop: this.getHeightOfLine(0) + }; + }, + + /** + * @private + */ + _wrapSVGTextAndBg: function(textAndBg) { + var noShadow = true, + textDecoration = this.getSvgTextDecoration(this); + return [ + textAndBg.textBgRects.join(''), + '\t\t', + textAndBg.textSpans.join(''), + '\n' + ]; + }, + + /** + * @private + * @param {Number} textTopOffset Text top offset + * @param {Number} textLeftOffset Text left offset + * @return {Object} + */ + _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { + var textSpans = [], + textBgRects = [], + height = textTopOffset, lineOffset; + // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineOffset = this._getLineLeftOffset(i); + if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { + this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); + } + this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height); + height += this.getHeightOfLine(i); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, left, top) { + var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex), + styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace), + fillStyles = styleProps ? 'style="' + styleProps + '"' : '', + dy = styleDecl.deltaY, dySpan = '', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + if (dy) { + dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" '; + } + return [ + '', + fabric.util.string.escapeXml(_char), + '' + ].join(''); + }, + + _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, style, + boxWidth = 0, + line = this._textLines[lineIndex], + timeToRender; + + textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight; + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + textLeftOffset += charBox.kernedWidth - charBox.width; + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle); + } + if (timeToRender) { + style = this._getStyleDeclaration(lineIndex, i) || { }; + textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); + charsToRender = ''; + actualStyle = nextStyle; + textLeftOffset += boxWidth; + boxWidth = 0; + } + } + }, + + _pushTextBgRect: function(textBgRects, color, left, top, width, height) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + }, + + _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) { + var line = this._textLines[i], + heightOfLine = this.getHeightOfLine(i) / this.lineHeight, + boxWidth = 0, + boxStart = 0, + charBox, currentColor, + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (currentColor !== lastColor) { + lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart, + textTopOffset, boxWidth, heightOfLine); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart, + textTopOffset, boxWidth, heightOfLine); + }, + + /** + * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + * + * @private + * @param {*} value + * @return {String} + */ + _getFillAttributes: function(value) { + var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0, lastHeight = 0; + for (var j = 0; j < lineIndex; j++) { + lineTopOffset += this.getHeightOfLine(j); + } + lastHeight = this.getHeightOfLine(j); + return { + lineTop: lineTopOffset, + offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) + }; + }, + + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow); + return svgStyle + ' white-space: pre;'; + }, + }); +})(); +/* _TO_SVG_END_ */ + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = {}); + + /** + * Textbox class, based on IText, allows the user to resize the text rectangle + * and wraps lines automatically. Textboxes have their Y scaling locked, the + * user can only change width. Height is adjusted automatically based on the + * wrapping of lines. + * @class fabric.Textbox + * @extends fabric.IText + * @mixes fabric.Observable + * @return {fabric.Textbox} thisArg + * @see {@link fabric.Textbox#initialize} for constructor definition + */ + fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, { + + /** + * Type of an object + * @type String + * @default + */ + type: 'textbox', + + /** + * Minimum width of textbox, in pixels. + * @type Number + * @default + */ + minWidth: 20, + + /** + * Minimum calculated width of a textbox, in pixels. + * fixed to 2 so that an empty textbox cannot go to 0 + * and is still selectable without text. + * @type Number + * @default + */ + dynamicMinWidth: 2, + + /** + * Cached array of text wrapping. + * @type Array + */ + __cachedLines: null, + + /** + * Override standard Object class values + */ + lockScalingFlip: true, + + /** + * Override standard Object class values + * Textbox needs this on false + */ + noScaleCache: false, + + /** + * Properties which when set cause object to change dimensions + * @type Object + * @private + */ + _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'), + + /** + * Use this regular expression to split strings in breakable lines + * @private + */ + _wordJoiners: /[ \t\r]/, + + /** + * Use this boolean property in order to split strings that have no white space concept. + * this is a cheap way to help with chinese/japanese + * @type Boolean + * @since 2.6.0 + */ + splitByGrapheme: false, + + /** + * Unlike superclass's version of this function, Textbox does not update + * its width. + * @private + * @override + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this._clearCache(); + // clear dynamicMinWidth as it will be different after we re-wrap line + this.dynamicMinWidth = 0; + // wrap lines + this._styleMap = this._generateStyleMap(this._splitText()); + // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap + if (this.dynamicMinWidth > this.width) { + this._set('width', this.dynamicMinWidth); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + // clear cache and re-calculate height + this.height = this.calcTextHeight(); + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Generate an object that translates the style object so that it is + * broken up by visual lines (new lines and automatic wrapping). + * The original text styles object is broken up by actual lines (new lines only), + * which is only sufficient for Text / IText + * @private + */ + _generateStyleMap: function(textInfo) { + var realLineCount = 0, + realLineCharCount = 0, + charCount = 0, + map = {}; + + for (var i = 0; i < textInfo.graphemeLines.length; i++) { + if (textInfo.graphemeText[charCount] === '\n' && i > 0) { + realLineCharCount = 0; + charCount++; + realLineCount++; + } + else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) { + // this case deals with space's that are removed from end of lines when wrapping + realLineCharCount++; + charCount++; + } + + map[i] = { line: realLineCount, offset: realLineCharCount }; + + charCount += textInfo.graphemeLines[i].length; + realLineCharCount += textInfo.graphemeLines[i].length; + } + + return map; + }, + + /** + * Returns true if object has a style property or has it on a specified line + * @param {Number} lineIndex + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (map) { + lineIndex = map.line; + } + } + return fabric.Text.prototype.styleHas.call(this, property, lineIndex); + }, + + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false, + map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1]; + if (map) { + lineIndex = map.line; + offset = map.offset; + } + if (mapNextLine) { + nextLineIndex = mapNextLine.line; + shouldLimit = nextLineIndex === lineIndex; + nextOffset = mapNextLine.offset; + } + obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + } + return true; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _getStyleDeclaration: function(lineIndex, charIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (!map) { + return null; + } + lineIndex = map.line; + charIndex = map.offset + charIndex; + } + return this.callSuper('_getStyleDeclaration', lineIndex, charIndex); + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + this.styles[lineIndex][charIndex] = style; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + delete this.styles[lineIndex][charIndex]; + }, + + /** + * probably broken need a fix + * Returns the real style line that correspond to the wrapped lineIndex line + * Used just to verify if the line does exist or not. + * @param {Number} lineIndex + * @returns {Boolean} if the line exists or not + * @private + */ + _getLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + return !!this.styles[map.line]; + }, + + /** + * Set the line style to an empty object so that is initialized + * @param {Number} lineIndex + * @param {Object} style + * @private + */ + _setLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + this.styles[map.line] = {}; + }, + + /** + * Wraps text using the 'width' property of Textbox. First this function + * splits text on newlines, so we preserve newlines entered by the user. + * Then it wraps each line using the width of the Textbox by calling + * _wrapLine(). + * @param {Array} lines The string array of text that is split into lines + * @param {Number} desiredWidth width you want to wrap to + * @returns {Array} Array of lines + */ + _wrapText: function(lines, desiredWidth) { + var wrapped = [], i; + this.isWrapping = true; + for (i = 0; i < lines.length; i++) { + wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); + } + this.isWrapping = false; + return wrapped; + }, + + /** + * Helper function to measure a string of text, given its lineIndex and charIndex offset + * it gets called when charBounds are not available yet. + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {number} lineIndex + * @param {number} charOffset + * @returns {number} + * @private + */ + _measureWord: function(word, lineIndex, charOffset) { + var width = 0, prevGrapheme, skipLeft = true; + charOffset = charOffset || 0; + for (var i = 0, len = word.length; i < len; i++) { + var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft); + width += box.kernedWidth; + prevGrapheme = word[i]; + } + return width; + }, + + /** + * Wraps a line of text using the width of the Textbox and a context. + * @param {Array} line The grapheme array that represent the line + * @param {Number} lineIndex + * @param {Number} desiredWidth width you want to wrap the line to + * @param {Number} reservedSpace space to remove from wrapping for custom functionalities + * @returns {Array} Array of line(s) into which the given text is wrapped + * to. + */ + _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) { + var lineWidth = 0, + splitByGrapheme = this.splitByGrapheme, + graphemeLines = [], + line = [], + // spaces in different languages? + words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), + word = '', + offset = 0, + infix = splitByGrapheme ? '' : ' ', + wordWidth = 0, + infixWidth = 0, + largestWordWidth = 0, + lineJustStarted = true, + additionalSpace = this._getWidthOfCharSpacing(), + reservedSpace = reservedSpace || 0; + // fix a difference between split and graphemeSplit + if (words.length === 0) { + words.push([]); + } + desiredWidth -= reservedSpace; + for (var i = 0; i < words.length; i++) { + // if using splitByGrapheme words are already in graphemes. + word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); + wordWidth = this._measureWord(word, lineIndex, offset); + offset += word.length; + + lineWidth += infixWidth + wordWidth - additionalSpace; + if (lineWidth > desiredWidth && !lineJustStarted) { + graphemeLines.push(line); + line = []; + lineWidth = wordWidth; + lineJustStarted = true; + } + else { + lineWidth += additionalSpace; + } + + if (!lineJustStarted && !splitByGrapheme) { + line.push(infix); + } + line = line.concat(word); + + infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); + offset++; + lineJustStarted = false; + // keep track of largest word + if (wordWidth > largestWordWidth) { + largestWordWidth = wordWidth; + } + } + + i && graphemeLines.push(line); + + if (largestWordWidth + reservedSpace > this.dynamicMinWidth) { + this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace; + } + return graphemeLines; + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @param {Number} lineIndex text to split + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + if (!this._styleMap[lineIndex + 1]) { + // is last line, return true; + return true; + } + if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) { + // this is last line before a line break, return true; + return true; + } + return false; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * @return Number + */ + missingNewlineOffset: function(lineIndex) { + if (this.splitByGrapheme) { + return this.isEndOfWrapping(lineIndex) ? 1 : 0; + } + return 1; + }, + + /** + * Gets lines of text to render in the Textbox. This function calculates + * text wrapping on the fly every time it is called. + * @param {String} text text to split + * @returns {Array} Array of lines in the Textbox. + * @override + */ + _splitTextIntoLines: function(text) { + var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text), + graphemeLines = this._wrapText(newText.lines, this.width), + lines = new Array(graphemeLines.length); + for (var i = 0; i < graphemeLines.length; i++) { + lines[i] = graphemeLines[i].join(''); + } + newText.lines = lines; + newText.graphemeLines = graphemeLines; + return newText; + }, + + getMinWidth: function() { + return Math.max(this.minWidth, this.dynamicMinWidth); + }, + + _removeExtraneousStyles: function() { + var linesToKeep = {}; + for (var prop in this._styleMap) { + if (this._textLines[prop]) { + linesToKeep[this._styleMap[prop].line] = 1; + } + } + for (var prop in this.styles) { + if (!linesToKeep[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)); + } + }); + + /** + * Returns fabric.Textbox instance from an object representation + * @static + * @memberOf fabric.Textbox + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created + */ + fabric.Textbox.fromObject = function(object, callback) { + return fabric.Object._fromObject('Textbox', object, callback, 'text'); + }; +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var controlsUtils = fabric.controlsUtils, + scaleSkewStyleHandler = controlsUtils.scaleSkewCursorStyleHandler, + scaleStyleHandler = controlsUtils.scaleCursorStyleHandler, + scalingEqually = controlsUtils.scalingEqually, + scalingYOrSkewingX = controlsUtils.scalingYOrSkewingX, + scalingXOrSkewingY = controlsUtils.scalingXOrSkewingY, + scaleOrSkewActionName = controlsUtils.scaleOrSkewActionName, + objectControls = fabric.Object.prototype.controls; + + objectControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mb = new fabric.Control({ + x: 0, + y: 0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); + + objectControls.mt = new fabric.Control({ + x: 0, + y: -0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); + + objectControls.tl = new fabric.Control({ + x: -0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.tr = new fabric.Control({ + x: 0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.bl = new fabric.Control({ + x: -0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.br = new fabric.Control({ + x: 0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + + objectControls.mtr = new fabric.Control({ + x: 0, + y: -0.5, + actionHandler: controlsUtils.rotationWithSnapping, + cursorStyleHandler: controlsUtils.rotationStyleHandler, + offsetY: -30, + withConnection: true, + actionName: 'rotate', + }); + + if (fabric.Textbox) { + // this is breaking the prototype inheritance, no time / ideas to fix it. + // is important to document that if you want to have all objects to have a + // specific custom control, you have to add it to Object prototype and to Textbox + // prototype. The controls are shared as references. So changes to control `tr` + // can still apply to all objects if needed. + var textBoxControls = fabric.Textbox.prototype.controls = { }; + + textBoxControls.mtr = objectControls.mtr; + textBoxControls.tr = objectControls.tr; + textBoxControls.br = objectControls.br; + textBoxControls.tl = objectControls.tl; + textBoxControls.bl = objectControls.bl; + textBoxControls.mt = objectControls.mt; + textBoxControls.mb = objectControls.mb; + + textBoxControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', + }); + + textBoxControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', + }); + } +})(); + diff --git a/frontend-web/vendor/js/guideLines.js b/frontend-web/vendor/js/guideLines.js new file mode 100644 index 0000000..d3e59de --- /dev/null +++ b/frontend-web/vendor/js/guideLines.js @@ -0,0 +1,219 @@ +function initAligningGuidelines(canvas) { + + var ctx = canvas.getSelectionContext(), + //aligningLineOffset = 0, + //aligningLineMargin = 0, + //aligningLineWidth = 1, + //aligningLineColor = 'rgb(0,0,0)', + aligningLineOffset = 4, + aligningLineMargin = 5,//吸引 + aligningLineWidth = 6, + aligningLineColor = '#6637FF', + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + let width = canvas.getWidth(); + let height = canvas.getHeight(); + ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 + ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 + //修复中心放大后的位置偏移 2021-10-24 + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on('mouse:down', function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on('object:moving', function(e) { + + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + + if (canvasObjects[i] === activeObject) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + } + + // snap by the left edge + if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the right edge + if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + } + + // snap by the top edge + if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + } + + // snap by the bottom edge + if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on('before:render', function() { + + if(canvas&&canvas.contextTop) + { + canvas.clearContext(canvas.contextTop); + } + + + }); + + canvas.on('after:render', function() { + for (var i = verticalLines.length; i--; ) { + drawVerticalLine(verticalLines[i]); + } + for (var i = horizontalLines.length; i--; ) { + drawHorizontalLine(horizontalLines[i]); + } + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on('mouse:up', function() { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + diff --git a/frontend-web/vendor/js/guideLines1.js b/frontend-web/vendor/js/guideLines1.js new file mode 100644 index 0000000..cb03b84 --- /dev/null +++ b/frontend-web/vendor/js/guideLines1.js @@ -0,0 +1,255 @@ +function initAligningGuidelines(canvas) { + + var ctx = canvas.getSelectionContext(), + //aligningLineOffset = 0, + //aligningLineMargin = 0, + //aligningLineWidth = 1, + //aligningLineColor = 'rgb(0,0,0)', + aligningLineOffset = 4, + aligningLineMargin = 5,//吸引 + aligningLineWidth = 6, + aligningLineColor = '#6637FF', + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + let width = canvas.getWidth(); + let height = canvas.getHeight(); + ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 + ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 + //修复中心放大后的位置偏移 2021-10-24 + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on('mouse:down', function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on('object:moving', function(e) { + + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // 清空辅助线数组,避免累积 + verticalLines.length = 0; + horizontalLines.length = 0; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + + if (canvasObjects[i] === activeObject) continue; + + // 跳过辅助线对象,避免辅助线之间相互对齐产生重影 + if (canvasObjects[i].isGuideLine) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + } + + // snap by the left edge + if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the right edge + if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + } + + // snap by the top edge + if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + } + + // snap by the bottom edge + if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on('before:render', function() { + + if(canvas&&canvas.contextTop) + { + canvas.clearContext(canvas.contextTop); + } + + + }); + + canvas.on('after:render', function() { + // 合并相同 x 坐标的竖向辅助线(使用四舍五入处理浮点数精度问题) + var mergedVerticalLines = {}; + for (var i = verticalLines.length; i--; ) { + var line = verticalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.x); + if (!mergedVerticalLines[key]) { + mergedVerticalLines[key] = { x: line.x, y1: line.y1, y2: line.y2 }; + } else { + // 合并 y 范围 + mergedVerticalLines[key].y1 = Math.min(mergedVerticalLines[key].y1, line.y1); + mergedVerticalLines[key].y2 = Math.max(mergedVerticalLines[key].y2, line.y2); + } + } + for (var key in mergedVerticalLines) { + drawVerticalLine(mergedVerticalLines[key]); + } + + // 合并相同 y 坐标的横向辅助线(使用四舍五入处理浮点数精度问题) + var mergedHorizontalLines = {}; + for (var i = horizontalLines.length; i--; ) { + var line = horizontalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.y); + if (!mergedHorizontalLines[key]) { + mergedHorizontalLines[key] = { y: line.y, x1: line.x1, x2: line.x2 }; + } else { + // 合并 x 范围 + mergedHorizontalLines[key].x1 = Math.min(mergedHorizontalLines[key].x1, line.x1); + mergedHorizontalLines[key].x2 = Math.max(mergedHorizontalLines[key].x2, line.x2); + } + } + for (var key in mergedHorizontalLines) { + drawHorizontalLine(mergedHorizontalLines[key]); + } + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on('mouse:up', function() { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + diff --git a/frontend-web/vendor/js/guideLines2.js b/frontend-web/vendor/js/guideLines2.js new file mode 100644 index 0000000..552fc5b --- /dev/null +++ b/frontend-web/vendor/js/guideLines2.js @@ -0,0 +1,255 @@ +function initAligningGuidelines(canvas) { + + var ctx = canvas.getSelectionContext(), + //aligningLineOffset = 0, + //aligningLineMargin = 0, + //aligningLineWidth = 1, + //aligningLineColor = 'rgb(0,0,0)', + aligningLineOffset = 4, + aligningLineMargin = 5,//吸引 + aligningLineWidth = 2, + aligningLineColor = '#6637FF', + viewportTransform, + zoom = 1; + + function drawVerticalLine(coords) { + drawLine( + coords.x + 0.5, + coords.y1 > coords.y2 ? coords.y2 : coords.y1, + coords.x + 0.5, + coords.y2 > coords.y1 ? coords.y2 : coords.y1); + } + + function drawHorizontalLine(coords) { + drawLine( + coords.x1 > coords.x2 ? coords.x2 : coords.x1, + coords.y + 0.5, + coords.x2 > coords.x1 ? coords.x2 : coords.x1, + coords.y + 0.5); + } + + function drawLine(x1, y1, x2, y2) { + ctx.save(); + ctx.lineWidth = aligningLineWidth; + ctx.strokeStyle = aligningLineColor; + ctx.beginPath(); + let width = canvas.getWidth(); + let height = canvas.getHeight(); + ctx.moveTo(width / 2 - ( ( width / 2 - (x1) ) * zoom ) , height / 2 - ( ( height / 2 - (y1) ) * zoom ));//哪个点散射出来 + ctx.lineTo(width / 2 - ( ( width / 2 - (x2) ) * zoom ), height / 2 - ( ( height / 2 - (y2) ) * zoom ));//结束到哪个点 + //修复中心放大后的位置偏移 2021-10-24 + ctx.stroke(); + ctx.restore(); + } + + function isInRange(value1, value2) { + value1 = Math.round(value1); + value2 = Math.round(value2); + for (var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) { + if (i === value2) { + return true; + } + } + return false; + } + + var verticalLines = [], + horizontalLines = []; + + canvas.on('mouse:down', function () { + viewportTransform = canvas.viewportTransform; + zoom = canvas.getZoom(); + }); + + canvas.on('object:moving', function(e) { + + var activeObject = e.target, + canvasObjects = canvas.getObjects(), + activeObjectCenter = activeObject.getCenterPoint(), + activeObjectLeft = activeObjectCenter.x, + activeObjectTop = activeObjectCenter.y, + activeObjectBoundingRect = activeObject.getBoundingRect(), + activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3], + activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0], + horizontalInTheRange = false, + verticalInTheRange = false, + transform = canvas._currentTransform; + + if (!transform) return; + + // 清空辅助线数组,避免累积 + verticalLines.length = 0; + horizontalLines.length = 0; + + // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions, + // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move + + for (var i = canvasObjects.length; i--; ) { + + if (canvasObjects[i] === activeObject) continue; + + // 跳过辅助线对象,避免辅助线之间相互对齐产生重影 + if (canvasObjects[i].isGuideLine) continue; + + var objectCenter = canvasObjects[i].getCenterPoint(), + objectLeft = objectCenter.x, + objectTop = objectCenter.y, + objectBoundingRect = canvasObjects[i].getBoundingRect(), + objectHeight = objectBoundingRect.height / viewportTransform[3], + objectWidth = objectBoundingRect.width / viewportTransform[0]; + + // snap by the horizontal center line + if (isInRange(objectLeft, activeObjectLeft)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), 'center', 'center'); + } + + // snap by the left edge + if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft - objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the right edge + if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) { + verticalInTheRange = true; + verticalLines.push({ + x: objectLeft + objectWidth / 2, + y1: (objectTop < activeObjectTop) + ? (objectTop - objectHeight / 2 - aligningLineOffset) + : (objectTop + objectHeight / 2 + aligningLineOffset), + y2: (activeObjectTop > objectTop) + ? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset) + : (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), 'center', 'center'); + } + + // snap by the vertical center line + if (isInRange(objectTop, activeObjectTop)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), 'center', 'center'); + } + + // snap by the top edge + if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop - objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), 'center', 'center'); + } + + // snap by the bottom edge + if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) { + horizontalInTheRange = true; + horizontalLines.push({ + y: objectTop + objectHeight / 2, + x1: (objectLeft < activeObjectLeft) + ? (objectLeft - objectWidth / 2 - aligningLineOffset) + : (objectLeft + objectWidth / 2 + aligningLineOffset), + x2: (activeObjectLeft > objectLeft) + ? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset) + : (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset) + }); + activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), 'center', 'center'); + } + } + + if (!horizontalInTheRange) { + horizontalLines.length = 0; + } + + if (!verticalInTheRange) { + verticalLines.length = 0; + } + }); + + canvas.on('before:render', function() { + + if(canvas&&canvas.contextTop) + { + canvas.clearContext(canvas.contextTop); + } + + + }); + + canvas.on('after:render', function() { + // 合并相同 x 坐标的竖向辅助线(使用四舍五入处理浮点数精度问题) + var mergedVerticalLines = {}; + for (var i = verticalLines.length; i--; ) { + var line = verticalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.x); + if (!mergedVerticalLines[key]) { + mergedVerticalLines[key] = { x: line.x, y1: line.y1, y2: line.y2 }; + } else { + // 合并 y 范围 + mergedVerticalLines[key].y1 = Math.min(mergedVerticalLines[key].y1, line.y1); + mergedVerticalLines[key].y2 = Math.max(mergedVerticalLines[key].y2, line.y2); + } + } + for (var key in mergedVerticalLines) { + drawVerticalLine(mergedVerticalLines[key]); + } + + // 合并相同 y 坐标的横向辅助线(使用四舍五入处理浮点数精度问题) + var mergedHorizontalLines = {}; + for (var i = horizontalLines.length; i--; ) { + var line = horizontalLines[i]; + // 使用四舍五入到整数作为 key,避免浮点数精度问题 + var key = Math.round(line.y); + if (!mergedHorizontalLines[key]) { + mergedHorizontalLines[key] = { y: line.y, x1: line.x1, x2: line.x2 }; + } else { + // 合并 x 范围 + mergedHorizontalLines[key].x1 = Math.min(mergedHorizontalLines[key].x1, line.x1); + mergedHorizontalLines[key].x2 = Math.max(mergedHorizontalLines[key].x2, line.x2); + } + } + for (var key in mergedHorizontalLines) { + drawHorizontalLine(mergedHorizontalLines[key]); + } + verticalLines.length = horizontalLines.length = 0; + }); + + canvas.on('mouse:up', function() { + verticalLines.length = horizontalLines.length = 0; + canvas.renderAll(); + }); +} + diff --git a/frontend-web/vendor/js/jr-qrcode.js b/frontend-web/vendor/js/jr-qrcode.js new file mode 100644 index 0000000..f3be5da --- /dev/null +++ b/frontend-web/vendor/js/jr-qrcode.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.jrQrcode=e():t.jrQrcode=e()}(this,function(){return function(t){function e(o){if(r[o])return r[o].exports;var n=r[o]={exports:{},id:o,loaded:!1};return t[o].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){"use strict";t.exports=r(3)},function(t,e){"use strict";!function(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(t){if(void 0===t||null===t)throw new TypeError("Cannot convert first argument to object");for(var e=Object(t),r=1;r=1&&n<=127?e+=t.charAt(r):n>2047?(e+=String.fromCharCode(224|n>>12&15),e+=String.fromCharCode(128|n>>6&63),e+=String.fromCharCode(128|n>>0&63)):(e+=String.fromCharCode(192|n>>6&31),e+=String.fromCharCode(128|n>>0&63));return e},t.utf8to16=function(t){var e,r,o,n,i,a;for(e="",o=t.length,r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:e+=t.charAt(r-1);break;case 12:case 13:i=t.charCodeAt(r++),e+=String.fromCharCode((31&n)<<6|63&i);break;case 14:i=t.charCodeAt(r++),a=t.charCodeAt(r++),e+=String.fromCharCode((15&n)<<12|(63&i)<<6|(63&a)<<0)}return e},t}()},function(t,e,r){"use strict";r(1),r(4);var o=r(2),n=function(){function t(t){var e=new QRCode(t.typeNumber,t.correctLevel);e.addData(t.text),e.make();var r=document.createElement("canvas");r.width=t.width,r.height=t.height;var o=r.getContext("2d"),n=(t.width-2*t.padding)/e.getModuleCount(),i=(t.height-2*t.padding)/e.getModuleCount();if(t.reverse){var a="rgba(0, 0, 0, 0)";o.fillStyle=a,t.foreground=a}else o.fillStyle=t.background;o.fillRect(0,0,r.width,r.height);for(var s=0;s=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=o.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,e)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4?this.modules[t+r][e+o]=!0:this.modules[t+r][e+o]=!1)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=f.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r),n=1;this.make();for(var i=0;i>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(var r=0;r<18;r++){var o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=f.getBCHTypeInfo(r),n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<6?this.modules[n][8]=i:n<8?this.modules[n+1][8]=i:this.modules[this.moduleCount-15+n][8]=i}for(var n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<8?this.modules[8][this.moduleCount-n-1]=i:n<9?this.modules[8][15-n-1+1]=i:this.modules[8][15-n-1]=i}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var u=!1;i>>n&1));var h=f.getMask(e,o,a-s);h&&(u=!u),this.modules[o][a-s]=u,n--,n==-1&&(i++,n=7)}if(o+=r,o<0||this.moduleCount<=o){o-=r,r=-r;break}}}},o.PAD0=236,o.PAD1=17,o.createData=function(t,e,r){for(var n=i.getRSBlocks(t,e),s=new a,u=0;u8*l)throw new Error("code length overflow. ("+s.getLengthInBits()+">"+8*l+")");for(s.getLengthInBits()+4<=8*l&&s.put(0,4);s.getLengthInBits()%8!=0;)s.putBit(!1);for(;;){if(s.getLengthInBits()>=8*l)break;if(s.put(o.PAD0,8),s.getLengthInBits()>=8*l)break;s.put(o.PAD1,8)}return o.createBytes(s,n)},o.createBytes=function(t,e){for(var r=0,o=0,i=0,a=new Array(e.length),s=new Array(e.length),u=0;u=0?v.get(m):0}}for(var p=0,g=0;g=0;)e^=f.G15<=0;)e^=f.G18<>>=1;return e},getPatternPosition:function(t){return f.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case h.PATTERN000:return(e+r)%2==0;case h.PATTERN001:return e%2==0;case h.PATTERN010:return r%3==0;case h.PATTERN011:return(e+r)%3==0;case h.PATTERN100:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case h.PATTERN101:return e*r%2+e*r%3==0;case h.PATTERN110:return(e*r%2+e*r%3)%2==0;case h.PATTERN111:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new n([1],0),r=0;r5&&(r+=3+i-5)}for(var o=0;o=256;)t-=255;return l.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},g=0;g<8;g++)l.EXP_TABLE[g]=1<>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}},window.QRCode=o,window.QRErrorCorrectLevel=u;try{t.exports={QRCode:o,QRErrorCorrectLevel:u}}catch(t){}}])}); \ No newline at end of file diff --git a/frontend-web/vendor/js/jsencrypt.min.js b/frontend-web/vendor/js/jsencrypt.min.js new file mode 100644 index 0000000..fc4d0dd --- /dev/null +++ b/frontend-web/vendor/js/jsencrypt.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.JSEncrypt={})}(this,function(t){"use strict";var e="0123456789abcdefghijklmnopqrstuvwxyz";function a(t){return e.charAt(t)}function i(t,e){return t&e}function u(t,e){return t|e}function r(t,e){return t^e}function n(t,e){return t&~e}function s(t){if(0==t)return-1;var e=0;return 0==(65535&t)&&(t>>=16,e+=16),0==(255&t)&&(t>>=8,e+=8),0==(15&t)&&(t>>=4,e+=4),0==(3&t)&&(t>>=2,e+=2),0==(1&t)&&++e,e}function o(t){for(var e=0;0!=t;)t&=t-1,++e;return e}var h="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function c(t){var e,i,r="";for(e=0;e+3<=t.length;e+=3)i=parseInt(t.substring(e,e+3),16),r+=h.charAt(i>>6)+h.charAt(63&i);for(e+1==t.length?(i=parseInt(t.substring(e,e+1),16),r+=h.charAt(i<<2)):e+2==t.length&&(i=parseInt(t.substring(e,e+2),16),r+=h.charAt(i>>2)+h.charAt((3&i)<<4));0<(3&r.length);)r+="=";return r}function f(t){var e,i="",r=0,n=0;for(e=0;e>2),n=3&s,r=1):1==r?(i+=a(n<<2|s>>4),n=15&s,r=2):2==r?(i+=a(n),i+=a(s>>2),n=3&s,r=3):(i+=a(n<<2|s>>4),i+=a(15&s),r=0))}return 1==r&&(i+=a(n<<2)),i}var l,p=function(t,e){return(p=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])})(t,e)};var g,d=function(t){var e;if(void 0===l){var i="0123456789ABCDEF",r=" \f\n\r\t \u2028\u2029";for(l={},e=0;e<16;++e)l[i.charAt(e)]=e;for(i=i.toLowerCase(),e=10;e<16;++e)l[i.charAt(e)]=e;for(e=0;e>16,r[r.length]=n>>8&255,r[r.length]=255&n,s=n=0):n<<=6}}switch(s){case 1:throw new Error("Base64 encoding incomplete: at least 2 bits missing");case 2:r[r.length]=n>>10;break;case 3:r[r.length]=n>>16,r[r.length]=n>>8&255}return r},re:/-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/,unarmor:function(t){var e=v.re.exec(t);if(e)if(e[1])t=e[1];else{if(!e[2])throw new Error("RegExp out of sync");t=e[2]}return v.decode(t)}},y=1e13,m=function(){function t(t){this.buf=[+t||0]}return t.prototype.mulAdd=function(t,e){var i,r,n=this.buf,s=n.length;for(i=0;ie&&(t=t.substring(0,e)+b),t}var w,D=function(){function i(t,e){this.hexDigits="0123456789ABCDEF",t instanceof i?(this.enc=t.enc,this.pos=t.pos):(this.enc=t,this.pos=e)}return i.prototype.get=function(t){if(void 0===t&&(t=this.pos++),t>=this.enc.length)throw new Error("Requesting byte offset "+t+" on a stream of length "+this.enc.length);return"string"==typeof this.enc?this.enc.charCodeAt(t):this.enc[t]},i.prototype.hexByte=function(t){return this.hexDigits.charAt(t>>4&15)+this.hexDigits.charAt(15&t)},i.prototype.hexDump=function(t,e,i){for(var r="",n=t;n>u&1?"1":"0";if(s.length>i)return n+E(s,i)}return n+s},i.prototype.parseOctetString=function(t,e,i){if(this.isASCII(t,e))return E(this.parseStringISO(t,e),i);var r=e-t,n="("+r+" byte)\n";(i/=2)i)return E(r,i);n=new m,s=0}}return 0>6,this.tagConstructed=0!=(32&e),this.tagNumber=31&e,31==this.tagNumber){for(var i=new m;e=t.get(),i.mulAdd(128,127&e),128&e;);this.tagNumber=i.simplify()}}return t.prototype.isUniversal=function(){return 0===this.tagClass},t.prototype.isEOC=function(){return 0===this.tagClass&&0===this.tagNumber},t}(),B=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],A=(1<<26)/B[B.length-1],O=function(){function b(t,e,i){null!=t&&("number"==typeof t?this.fromNumber(t,e,i):null==e&&"string"!=typeof t?this.fromString(t,256):this.fromString(t,e))}return b.prototype.toString=function(t){if(this.s<0)return"-"+this.negate().toString(t);var e;if(16==t)e=4;else if(8==t)e=3;else if(2==t)e=1;else if(32==t)e=5;else{if(4!=t)return this.toRadix(t);e=2}var i,r=(1<>h)&&(n=!0,s=a(i));0<=o;)h>(h+=this.DB-e)):(i=this[o]>>(h-=e)&r,h<=0&&(h+=this.DB,--o)),0>24},b.prototype.shortValue=function(){return 0==this.t?this.s:this[0]<<16>>16},b.prototype.signum=function(){return this.s<0?-1:this.t<=0||1==this.t&&this[0]<=0?0:1},b.prototype.toByteArray=function(){var t=this.t,e=[];e[0]=this.s;var i,r=this.DB-t*this.DB%8,n=0;if(0>r)!=(this.s&this.DM)>>r&&(e[n++]=i|this.s<>(r+=this.DB-8)):(i=this[t]>>(r-=8)&255,r<=0&&(r+=this.DB,--t)),0!=(128&i)&&(i|=-256),0==n&&(128&this.s)!=(128&i)&&++n,(0=this.t?0!=this.s:0!=(this[e]&1<>n-a&u:(f=(t[p]&(1<>this.DB+n-a)),h=i;0==(1&f);)f>>=1,--h;if((n-=h)<0&&(n+=this.DB,--p),g)o[f].copyTo(s),g=!1;else{for(;1this.DB?(this[this.t-1]|=(o&(1<>this.DB-s):this[this.t-1]|=o<=this.DB&&(s-=this.DB))}8==i&&0!=(128&+t[0])&&(this.s=-1,0>r|o,o=(this[h]&n)<=this.t)e.t=0;else{var r=t%this.DB,n=this.DB-r,s=(1<>r;for(var o=i+1;o>r;0>=this.DB;if(t.t>=this.DB;r+=this.s}else{for(r+=this.s;i>=this.DB;r-=t.s}e.s=r<0?-1:0,r<-1?e[i++]=this.DV+r:0=e.DV&&(t[i+e.t]-=e.DV,t[i+e.t+1]=1)}0>this.F2:0),l=this.FV/f,p=(1<=i&&(this.dMultiply(r),this.dAddOffset(o,0),o=s=0))}0t&&this.subTo(b.ONE.shiftLeft(t-1),this);else{var r=[],n=7&t;r.length=1+(t>>3),e.nextBytes(r),0>=this.DB;if(t.t>=this.DB;r+=this.s}else{for(r+=this.s;i>=this.DB;r+=t.s}e.s=r<0?-1:0,0=this.DV;)this[e]-=this.DV,++e>=this.t&&(this[this.t++]=0),++this[e]}},b.prototype.multiplyLowerTo=function(t,e,i){var r=Math.min(this.t+t.t,e);for(i.s=0,i.t=r;0>1)&&(t=B.length);for(var n=M(),s=0;st&&n.subTo(b.ONE.shiftLeft(t-1),n),n.isProbablePrime(e)?setTimeout(function(){r()},0):setTimeout(s,0)};setTimeout(s,0)}else{var o=[],h=7&t;o.length=1+(t>>3),e.nextBytes(o),0>15,this.um=(1<>15)*this.mpl&this.um)<<15)&t.DM;for(t[i=e+this.m.t]+=this.m.am(0,r,t,e,0,this.m.t);t[i]>=t.DV;)t[i]-=t.DV,t[++i]++}t.clamp(),t.drShiftTo(this.m.t,t),0<=t.compareTo(this.m)&&t.subTo(this.m,t)},t.prototype.mulTo=function(t,e,i){t.multiplyTo(e,i),this.reduce(i)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}(),P=function(){function t(t){this.m=t,this.r2=M(),this.q3=M(),O.ONE.dlShiftTo(2*t.t,this.r2),this.mu=this.r2.divide(t)}return t.prototype.convert=function(t){if(t.s<0||t.t>2*this.m.t)return t.mod(this.m);if(t.compareTo(this.m)<0)return t;var e=M();return t.copyTo(e),this.reduce(e),e},t.prototype.revert=function(t){return t},t.prototype.reduce=function(t){for(t.drShiftTo(this.m.t-1,this.r2),t.t>this.m.t+1&&(t.t=this.m.t+1,t.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);t.compareTo(this.r2)<0;)t.dAddOffset(1,this.m.t+1);for(t.subTo(this.r2,t);0<=t.compareTo(this.m);)t.subTo(this.m,t)},t.prototype.mulTo=function(t,e,i){t.multiplyTo(e,i),this.reduce(i)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}();function M(){return new O(null)}function L(t,e){return new O(t,e)}"Microsoft Internet Explorer"==navigator.appName?(O.prototype.am=function(t,e,i,r,n,s){for(var o=32767&e,h=e>>15;0<=--s;){var a=32767&this[t],u=this[t++]>>15,c=h*a+u*o;n=((a=o*a+((32767&c)<<15)+i[r]+(1073741823&n))>>>30)+(c>>>15)+h*u+(n>>>30),i[r++]=1073741823&a}return n},w=30):"Netscape"!=navigator.appName?(O.prototype.am=function(t,e,i,r,n,s){for(;0<=--s;){var o=e*this[t++]+i[r]+n;n=Math.floor(o/67108864),i[r++]=67108863&o}return n},w=26):(O.prototype.am=function(t,e,i,r,n,s){for(var o=16383&e,h=e>>14;0<=--s;){var a=16383&this[t],u=this[t++]>>14,c=h*a+u*o;n=((a=o*a+((16383&c)<<14)+i[r]+n)>>28)+(c>>14)+h*u,i[r++]=268435455&a}return n},w=28),O.prototype.DB=w,O.prototype.DM=(1<>>16)&&(t=e,i+=16),0!=(e=t>>8)&&(t=e,i+=8),0!=(e=t>>4)&&(t=e,i+=4),0!=(e=t>>2)&&(t=e,i+=2),0!=(e=t>>1)&&(t=e,i+=1),i}O.ZERO=K(0),O.ONE=K(1);var U=function(){function t(){this.i=0,this.j=0,this.S=[]}return t.prototype.init=function(t){var e,i,r;for(e=0;e<256;++e)this.S[e]=e;for(e=i=0;e<256;++e)i=i+this.S[e]+t[e%t.length]&255,r=this.S[e],this.S[e]=this.S[i],this.S[i]=r;this.i=0,this.j=0},t.prototype.next=function(){var t;return this.i=this.i+1&255,this.j=this.j+this.S[this.i]&255,t=this.S[this.i],this.S[this.i]=this.S[this.j],this.S[this.j]=t,this.S[t+this.S[this.i]&255]},t}();var k,_,z=256,Z=null;if(null==Z){Z=[];var G=void(_=0);if(window.crypto&&window.crypto.getRandomValues){var $=new Uint32Array(256);for(window.crypto.getRandomValues($),G=0;G<$.length;++G)Z[_++]=255&$[G]}var Y=function(t){if(this.count=this.count||0,256<=this.count||z<=_)window.removeEventListener?window.removeEventListener("mousemove",Y,!1):window.detachEvent&&window.detachEvent("onmousemove",Y);else try{var e=t.x+t.y;Z[_++]=255&e,this.count+=1}catch(t){}};window.addEventListener?window.addEventListener("mousemove",Y,!1):window.attachEvent&&window.attachEvent("onmousemove",Y)}function J(){if(null==k){for(k=new U;_>6|192):(i[--e]=63&n|128,i[--e]=n>>6&63|128,i[--e]=n>>12|224)}i[--e]=0;for(var s=new X,o=[];2>3);if(null==e)return null;var i=this.doPublic(e);if(null==i)return null;var r=i.toString(16);return 0==(1&r.length)?r:"0"+r},t.prototype.encryptLong=function(t){try{var e="",i=new Array;i.push(0);for(var r=0,n=t.length,s=void 0,o=0,h=0;h>3;t=f(t);try{if(t.length>e){var r="";return t.match(/.{1,256}/g).forEach(function(t){var e=i.decrypt(t);r+=e}),r}return this.decrypt(t)}catch(t){return!1}},t.prototype.setPrivate=function(t,e,i){null!=t&&null!=e&&0>1;this.e=parseInt(e,16);for(var n=new O(e,16);;){for(;this.p=new O(t-r,1,i),0!=this.p.subtract(O.ONE).gcd(n).compareTo(O.ONE)||!this.p.isProbablePrime(10););for(;this.q=new O(r,1,i),0!=this.q.subtract(O.ONE).gcd(n).compareTo(O.ONE)||!this.q.isProbablePrime(10););if(this.p.compareTo(this.q)<=0){var s=this.p;this.p=this.q,this.q=s}var o=this.p.subtract(O.ONE),h=this.q.subtract(O.ONE),a=o.multiply(h);if(0==a.gcd(n).compareTo(O.ONE)){this.n=this.p.multiply(this.q),this.d=n.modInverse(a),this.dmp1=this.d.mod(o),this.dmq1=this.d.mod(h),this.coeff=this.q.modInverse(this.p);break}}},t.prototype.decrypt=function(t){var e=L(t,16),i=this.doPrivate(e);return null==i?null:function(t,e){var i=t.toByteArray(),r=0;for(;r=i.length)return null;var n="";for(;++r>3)},t.prototype.generateAsync=function(t,e,n){var s=new X,o=t>>1;this.e=parseInt(e,16);var h=new O(e,16),a=this,u=function(){var e=function(){if(a.p.compareTo(a.q)<=0){var t=a.p;a.p=a.q,a.q=t}var e=a.p.subtract(O.ONE),i=a.q.subtract(O.ONE),r=e.multiply(i);0==r.gcd(h).compareTo(O.ONE)?(a.n=a.p.multiply(a.q),a.d=h.modInverse(r),a.dmp1=a.d.mod(e),a.dmq1=a.d.mod(i),a.coeff=a.q.modInverse(a.p),setTimeout(function(){n()},0)):setTimeout(u,0)},i=function(){a.q=M(),a.q.fromNumberAsync(o,1,s,function(){a.q.subtract(O.ONE).gcda(h,function(t){0==t.compareTo(O.ONE)&&a.q.isProbablePrime(10)?setTimeout(e,0):setTimeout(i,0)})})},r=function(){a.p=M(),a.p.fromNumberAsync(t-o,1,s,function(){a.p.subtract(O.ONE).gcda(h,function(t){0==t.compareTo(O.ONE)&&a.p.isProbablePrime(10)?setTimeout(i,0):setTimeout(r,0)})})};setTimeout(r,0)};setTimeout(u,0)},t.prototype.sign=function(t,e,i){var r=function(t,e){if(e=e?t:new Array(e-t.length+1).join("0")+t},this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=stohex(t)},this.setByDateValue=function(t,e,i,r,n,s){var o=new Date(Date.UTC(t,e-1,i,r,n,s,0));this.setByDate(o)},this.getFreshValueHex=function(){return this.hV}},tt.lang.extend(et.asn1.DERAbstractTime,et.asn1.ASN1Object),et.asn1.DERAbstractStructured=function(t){et.asn1.DERAbstractString.superclass.constructor.call(this),this.setByASN1ObjectArray=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array=t},this.appendASN1Object=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array.push(t)},this.asn1Array=new Array,void 0!==t&&void 0!==t.array&&(this.asn1Array=t.array)},tt.lang.extend(et.asn1.DERAbstractStructured,et.asn1.ASN1Object),et.asn1.DERBoolean=function(){et.asn1.DERBoolean.superclass.constructor.call(this),this.hT="01",this.hTLV="0101ff"},tt.lang.extend(et.asn1.DERBoolean,et.asn1.ASN1Object),et.asn1.DERInteger=function(t){et.asn1.DERInteger.superclass.constructor.call(this),this.hT="02",this.setByBigInteger=function(t){this.hTLV=null,this.isModified=!0,this.hV=et.asn1.ASN1Util.bigIntToMinTwosComplementsHex(t)},this.setByInteger=function(t){var e=new O(String(t),10);this.setByBigInteger(e)},this.setValueHex=function(t){this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&(void 0!==t.bigint?this.setByBigInteger(t.bigint):void 0!==t.int?this.setByInteger(t.int):"number"==typeof t?this.setByInteger(t):void 0!==t.hex&&this.setValueHex(t.hex))},tt.lang.extend(et.asn1.DERInteger,et.asn1.ASN1Object),et.asn1.DERBitString=function(t){if(void 0!==t&&void 0!==t.obj){var e=et.asn1.ASN1Util.newObject(t.obj);t.hex="00"+e.getEncodedHex()}et.asn1.DERBitString.superclass.constructor.call(this),this.hT="03",this.setHexValueIncludingUnusedBits=function(t){this.hTLV=null,this.isModified=!0,this.hV=t},this.setUnusedBitsAndHexValue=function(t,e){if(t<0||7, https://github.com/MrRio/jsPDF + * 2015-2021 yWorks GmbH, http://www.yworks.com + * 2015-2021 Lukas Holländer , https://github.com/HackbrettXXX + * 2016-2018 Aras Abbasi + * 2010 Aaron Spike, https://github.com/acspike + * 2012 Willow Systems Corporation, https://github.com/willowsystems + * 2012 Pablo Hess, https://github.com/pablohess + * 2012 Florian Jenett, https://github.com/fjenett + * 2013 Warren Weckesser, https://github.com/warrenweckesser + * 2013 Youssef Beddad, https://github.com/lifof + * 2013 Lee Driscoll, https://github.com/lsdriscoll + * 2013 Stefan Slonevskiy, https://github.com/stefslon + * 2013 Jeremy Morel, https://github.com/jmorel + * 2013 Christoph Hartmann, https://github.com/chris-rock + * 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria + * 2014 James Makes, https://github.com/dollaruw + * 2014 Diego Casorran, https://github.com/diegocr + * 2014 Steven Spungin, https://github.com/Flamenco + * 2014 Kenneth Glassey, https://github.com/Gavvers + * + * 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. + * + * Contributor(s): + * siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango, + * kim3er, mfo, alnorth, Flamenco + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).jspdf={})}(this,(function(t){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var r=function(){return"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this}();function n(){r.console&&"function"==typeof r.console.log&&r.console.log.apply(r.console,arguments)}var i={log:n,warn:function(t){r.console&&("function"==typeof r.console.warn?r.console.warn.apply(r.console,arguments):n.call(null,arguments))},error:function(t){r.console&&("function"==typeof r.console.error?r.console.error.apply(r.console,arguments):n(t))}};function a(t,e,r){var n=new XMLHttpRequest;n.open("GET",t),n.responseType="blob",n.onload=function(){l(n.response,e,r)},n.onerror=function(){i.error("could not download file")},n.send()}function o(t){var e=new XMLHttpRequest;e.open("HEAD",t,!1);try{e.send()}catch(t){}return e.status>=200&&e.status<=299}function s(t){try{t.dispatchEvent(new MouseEvent("click"))}catch(r){var e=document.createEvent("MouseEvents");e.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),t.dispatchEvent(e)}}var c,u,l=r.saveAs||("object"!==("undefined"==typeof window?"undefined":e(window))||window!==r?function(){}:"undefined"!=typeof HTMLAnchorElement&&"download"in HTMLAnchorElement.prototype?function(t,e,n){var i=r.URL||r.webkitURL,c=document.createElement("a");e=e||t.name||"download",c.download=e,c.rel="noopener","string"==typeof t?(c.href=t,c.origin!==location.origin?o(c.href)?a(t,e,n):s(c,c.target="_blank"):s(c)):(c.href=i.createObjectURL(t),setTimeout((function(){i.revokeObjectURL(c.href)}),4e4),setTimeout((function(){s(c)}),0))}:"msSaveOrOpenBlob"in navigator?function(t,r,n){if(r=r||t.name||"download","string"==typeof t)if(o(t))a(t,r,n);else{var c=document.createElement("a");c.href=t,c.target="_blank",setTimeout((function(){s(c)}))}else navigator.msSaveOrOpenBlob(function(t,r){return void 0===r?r={autoBom:!1}:"object"!==e(r)&&(i.warn("Deprecated: Expected third argument to be a object"),r={autoBom:!r}),r.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(t.type)?new Blob([String.fromCharCode(65279),t],{type:t.type}):t}(t,n),r)}:function(t,n,i,o){if((o=o||open("","_blank"))&&(o.document.title=o.document.body.innerText="downloading..."),"string"==typeof t)return a(t,n,i);var s="application/octet-stream"===t.type,c=/constructor/i.test(r.HTMLElement)||r.safari,u=/CriOS\/[\d]+/.test(navigator.userAgent);if((u||s&&c)&&"object"===("undefined"==typeof FileReader?"undefined":e(FileReader))){var l=new FileReader;l.onloadend=function(){var t=l.result;t=u?t:t.replace(/^data:[^;]*;/,"data:attachment/file;"),o?o.location.href=t:location=t,o=null},l.readAsDataURL(t)}else{var h=r.URL||r.webkitURL,f=h.createObjectURL(t);o?o.location=f:location.href=f,o=null,setTimeout((function(){h.revokeObjectURL(f)}),4e4)}}); +/** + * A class to parse color values + * @author Stoyan Stefanov + * {@link http://www.phpied.com/rgb-color-parser-in-javascript/} + * @license Use it if you like it + */function h(t){var e;t=t||"",this.ok=!1,"#"==t.charAt(0)&&(t=t.substr(1,6));t={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"}[t=(t=t.replace(/ /g,"")).toLowerCase()]||t;for(var r=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(t){return[parseInt(t[1]),parseInt(t[2]),parseInt(t[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}}],n=0;n255?255:this.r,this.g=this.g<0||isNaN(this.g)?0:this.g>255?255:this.g,this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b,this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"},this.toHex=function(){var t=this.r.toString(16),e=this.g.toString(16),r=this.b.toString(16);return 1==t.length&&(t="0"+t),1==e.length&&(e="0"+e),1==r.length&&(r="0"+r),"#"+t+e+r}} +/** + * @license + * Joseph Myers does not specify a particular license for his work. + * + * Author: Joseph Myers + * Accessed from: http://www.myersdaily.org/joseph/javascript/md5.js + * + * Modified by: Owen Leong + */ +function f(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r=p(r,n,i,a,e[0],7,-680876936),a=p(a,r,n,i,e[1],12,-389564586),i=p(i,a,r,n,e[2],17,606105819),n=p(n,i,a,r,e[3],22,-1044525330),r=p(r,n,i,a,e[4],7,-176418897),a=p(a,r,n,i,e[5],12,1200080426),i=p(i,a,r,n,e[6],17,-1473231341),n=p(n,i,a,r,e[7],22,-45705983),r=p(r,n,i,a,e[8],7,1770035416),a=p(a,r,n,i,e[9],12,-1958414417),i=p(i,a,r,n,e[10],17,-42063),n=p(n,i,a,r,e[11],22,-1990404162),r=p(r,n,i,a,e[12],7,1804603682),a=p(a,r,n,i,e[13],12,-40341101),i=p(i,a,r,n,e[14],17,-1502002290),r=g(r,n=p(n,i,a,r,e[15],22,1236535329),i,a,e[1],5,-165796510),a=g(a,r,n,i,e[6],9,-1069501632),i=g(i,a,r,n,e[11],14,643717713),n=g(n,i,a,r,e[0],20,-373897302),r=g(r,n,i,a,e[5],5,-701558691),a=g(a,r,n,i,e[10],9,38016083),i=g(i,a,r,n,e[15],14,-660478335),n=g(n,i,a,r,e[4],20,-405537848),r=g(r,n,i,a,e[9],5,568446438),a=g(a,r,n,i,e[14],9,-1019803690),i=g(i,a,r,n,e[3],14,-187363961),n=g(n,i,a,r,e[8],20,1163531501),r=g(r,n,i,a,e[13],5,-1444681467),a=g(a,r,n,i,e[2],9,-51403784),i=g(i,a,r,n,e[7],14,1735328473),r=m(r,n=g(n,i,a,r,e[12],20,-1926607734),i,a,e[5],4,-378558),a=m(a,r,n,i,e[8],11,-2022574463),i=m(i,a,r,n,e[11],16,1839030562),n=m(n,i,a,r,e[14],23,-35309556),r=m(r,n,i,a,e[1],4,-1530992060),a=m(a,r,n,i,e[4],11,1272893353),i=m(i,a,r,n,e[7],16,-155497632),n=m(n,i,a,r,e[10],23,-1094730640),r=m(r,n,i,a,e[13],4,681279174),a=m(a,r,n,i,e[0],11,-358537222),i=m(i,a,r,n,e[3],16,-722521979),n=m(n,i,a,r,e[6],23,76029189),r=m(r,n,i,a,e[9],4,-640364487),a=m(a,r,n,i,e[12],11,-421815835),i=m(i,a,r,n,e[15],16,530742520),r=v(r,n=m(n,i,a,r,e[2],23,-995338651),i,a,e[0],6,-198630844),a=v(a,r,n,i,e[7],10,1126891415),i=v(i,a,r,n,e[14],15,-1416354905),n=v(n,i,a,r,e[5],21,-57434055),r=v(r,n,i,a,e[12],6,1700485571),a=v(a,r,n,i,e[3],10,-1894986606),i=v(i,a,r,n,e[10],15,-1051523),n=v(n,i,a,r,e[1],21,-2054922799),r=v(r,n,i,a,e[8],6,1873313359),a=v(a,r,n,i,e[15],10,-30611744),i=v(i,a,r,n,e[6],15,-1560198380),n=v(n,i,a,r,e[13],21,1309151649),r=v(r,n,i,a,e[4],6,-145523070),a=v(a,r,n,i,e[11],10,-1120210379),i=v(i,a,r,n,e[2],15,718787259),n=v(n,i,a,r,e[9],21,-343485551),t[0]=S(r,t[0]),t[1]=S(n,t[1]),t[2]=S(i,t[2]),t[3]=S(a,t[3])}function d(t,e,r,n,i,a){return e=S(S(e,t),S(n,a)),S(e<>>32-i,r)}function p(t,e,r,n,i,a,o){return d(e&r|~e&n,t,e,i,a,o)}function g(t,e,r,n,i,a,o){return d(e&n|r&~n,t,e,i,a,o)}function m(t,e,r,n,i,a,o){return d(e^r^n,t,e,i,a,o)}function v(t,e,r,n,i,a,o){return d(r^(e|~n),t,e,i,a,o)}function b(t){var e,r=t.length,n=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=t.length;e+=64)f(n,y(t.substring(e-64,e)));t=t.substring(e-64);var i=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e>2]|=t.charCodeAt(e)<<(e%4<<3);if(i[e>>2]|=128<<(e%4<<3),e>55)for(f(n,i),e=0;e<16;e++)i[e]=0;return i[14]=8*r,f(n,i),n}function y(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}c=r.atob.bind(r),u=r.btoa.bind(r);var w="0123456789abcdef".split("");function N(t){for(var e="",r=0;r<4;r++)e+=w[t>>8*r+4&15]+w[t>>8*r&15];return e}function L(t){return String.fromCharCode((255&t)>>0,(65280&t)>>8,(16711680&t)>>16,(4278190080&t)>>24)}function A(t){return function(t){return t.map(L).join("")}(b(t))}var x="5d41402abc4b2a76b9719d911017c592"!=function(t){for(var e=0;e>16)+(e>>16)+(r>>16)<<16|65535&r}return t+e&4294967295} +/** + * @license + * FPDF is released under a permissive license: there is no usage restriction. + * You may embed it freely in your application (commercial or not), with or + * without modifications. + * + * Reference: http://www.fpdf.org/en/script/script37.php + */function _(t,e){var r,n,i,a;if(t!==r){for(var o=(i=t,a=1+(256/t.length>>0),new Array(a+1).join(i)),s=[],c=0;c<256;c++)s[c]=c;var u=0;for(c=0;c<256;c++){var l=s[c];u=(u+l+o.charCodeAt(c))%256,s[c]=s[u],s[u]=l}r=t,n=s}else s=n;var h=e.length,f=0,d=0,p="";for(c=0;c€/\f©þdSiz";var a=(e+this.padding).substr(0,32),o=(r+this.padding).substr(0,32);this.O=this.processOwnerPassword(a,o),this.P=-(1+(255^i)),this.encryptionKey=A(a+this.O+this.lsbFirstWord(this.P)+this.hexToBytes(n)).substr(0,5),this.U=_(this.encryptionKey,this.padding)}function F(t){if(/[^\u0000-\u00ff]/.test(t))throw new Error("Invalid PDF Name Object: "+t+", Only accept ASCII characters.");for(var e="",r=t.length,n=0;n126)e+="#"+("0"+i.toString(16)).slice(-2);else e+=t[n]}return e}function I(t){if("object"!==e(t))throw new Error("Invalid Context passed to initialize PubSub (jsPDF-module)");var n={};this.subscribe=function(t,e,r){if(r=r||!1,"string"!=typeof t||"function"!=typeof e||"boolean"!=typeof r)throw new Error("Invalid arguments passed to PubSub.subscribe (jsPDF-module)");n.hasOwnProperty(t)||(n[t]={});var i=Math.random().toString(35);return n[t][i]=[e,!!r],i},this.unsubscribe=function(t){for(var e in n)if(n[e][t])return delete n[e][t],0===Object.keys(n[e]).length&&delete n[e],!0;return!1},this.publish=function(e){if(n.hasOwnProperty(e)){var a=Array.prototype.slice.call(arguments,1),o=[];for(var s in n[e]){var c=n[e][s];try{c[0].apply(t,a)}catch(t){r.console&&i.error("jsPDF PubSub Error",t.message,t)}c[1]&&o.push(s)}o.length&&o.forEach(this.unsubscribe)}},this.getTopics=function(){return n}}function C(t){if(!(this instanceof C))return new C(t);var e="opacity,stroke-opacity".split(",");for(var r in t)t.hasOwnProperty(r)&&e.indexOf(r)>=0&&(this[r]=t[r]);this.id="",this.objectNumber=-1}function j(t,e){this.gState=t,this.matrix=e,this.id="",this.objectNumber=-1}function O(t,e,r,n,i){if(!(this instanceof O))return new O(t,e,r,n,i);this.type="axial"===t?2:3,this.coords=e,this.colors=r,j.call(this,n,i)}function B(t,e,r,n,i){if(!(this instanceof B))return new B(t,e,r,n,i);this.boundingBox=t,this.xStep=e,this.yStep=r,this.stream="",this.cloneIndex=0,j.call(this,n,i)}function M(t){var n,a="string"==typeof arguments[0]?arguments[0]:"p",o=arguments[1],s=arguments[2],c=arguments[3],f=[],d=1,p=16,g="S",m=null;"object"===e(t=t||{})&&(a=t.orientation,o=t.unit||o,s=t.format||s,c=t.compress||t.compressPdf||c,null!==(m=t.encryption||null)&&(m.userPassword=m.userPassword||"",m.ownerPassword=m.ownerPassword||"",m.userPermissions=m.userPermissions||[]),d="number"==typeof t.userUnit?Math.abs(t.userUnit):1,void 0!==t.precision&&(n=t.precision),void 0!==t.floatPrecision&&(p=t.floatPrecision),g=t.defaultPathOperation||"S"),f=t.filters||(!0===c?["FlateEncode"]:f),o=o||"mm",a=(""+(a||"P")).toLowerCase();var v=t.putOnlyUsedFonts||!1,b={},y={internal:{},__private__:{}};y.__private__.PubSub=I;var w="1.3",N=y.__private__.getPdfVersion=function(){return w};y.__private__.setPdfVersion=function(t){w=t};var L={a0:[2383.94,3370.39],a1:[1683.78,2383.94],a2:[1190.55,1683.78],a3:[841.89,1190.55],a4:[595.28,841.89],a5:[419.53,595.28],a6:[297.64,419.53],a7:[209.76,297.64],a8:[147.4,209.76],a9:[104.88,147.4],a10:[73.7,104.88],b0:[2834.65,4008.19],b1:[2004.09,2834.65],b2:[1417.32,2004.09],b3:[1000.63,1417.32],b4:[708.66,1000.63],b5:[498.9,708.66],b6:[354.33,498.9],b7:[249.45,354.33],b8:[175.75,249.45],b9:[124.72,175.75],b10:[87.87,124.72],c0:[2599.37,3676.54],c1:[1836.85,2599.37],c2:[1298.27,1836.85],c3:[918.43,1298.27],c4:[649.13,918.43],c5:[459.21,649.13],c6:[323.15,459.21],c7:[229.61,323.15],c8:[161.57,229.61],c9:[113.39,161.57],c10:[79.37,113.39],dl:[311.81,623.62],letter:[612,792],"government-letter":[576,756],legal:[612,1008],"junior-legal":[576,360],ledger:[1224,792],tabloid:[792,1224],"credit-card":[153,243]};y.__private__.getPageFormats=function(){return L};var A=y.__private__.getPageFormat=function(t){return L[t]};s=s||"a4";var x={COMPAT:"compat",ADVANCED:"advanced"},S=x.COMPAT;function _(){this.saveGraphicsState(),ht(new Vt(_t,0,0,-_t,0,Rr()*_t).toString()+" cm"),this.setFontSize(this.getFontSize()/_t),g="n",S=x.ADVANCED}function P(){this.restoreGraphicsState(),g="S",S=x.COMPAT}var j=y.__private__.combineFontStyleAndFontWeight=function(t,e){if("bold"==t&&"normal"==e||"bold"==t&&400==e||"normal"==t&&"italic"==e||"bold"==t&&"italic"==e)throw new Error("Invalid Combination of fontweight and fontstyle");return e&&(t=400==e||"normal"===e?"italic"===t?"italic":"normal":700!=e&&"bold"!==e||"normal"!==t?(700==e?"bold":e)+""+t:"bold"),t};y.advancedAPI=function(t){var e=S===x.COMPAT;return e&&_.call(this),"function"!=typeof t||(t(this),e&&P.call(this)),this},y.compatAPI=function(t){var e=S===x.ADVANCED;return e&&P.call(this),"function"!=typeof t||(t(this),e&&_.call(this)),this},y.isAdvancedAPI=function(){return S===x.ADVANCED};var E,q=function(t){if(S!==x.ADVANCED)throw new Error(t+" is only available in 'advanced' API mode. You need to call advancedAPI() first.")},D=y.roundToPrecision=y.__private__.roundToPrecision=function(t,e){var r=n||e;if(isNaN(t)||isNaN(r))throw new Error("Invalid argument passed to jsPDF.roundToPrecision");return t.toFixed(r).replace(/0+$/,"")};E=y.hpf=y.__private__.hpf="number"==typeof p?function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,p)}:"smart"===p?function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,t>-1&&t<1?16:5)}:function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,16)};var R=y.f2=y.__private__.f2=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.f2");return D(t,2)},T=y.__private__.f3=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.f3");return D(t,3)},U=y.scale=y.__private__.scale=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.scale");return S===x.COMPAT?t*_t:S===x.ADVANCED?t:void 0},z=function(t){return S===x.COMPAT?Rr()-t:S===x.ADVANCED?t:void 0},H=function(t){return U(z(t))};y.__private__.setPrecision=y.setPrecision=function(t){"number"==typeof parseInt(t,10)&&(n=parseInt(t,10))};var W,V="00000000000000000000000000000000",G=y.__private__.getFileId=function(){return V},Y=y.__private__.setFileId=function(t){return V=void 0!==t&&/^[a-fA-F0-9]{32}$/.test(t)?t.toUpperCase():V.split("").map((function(){return"ABCDEF0123456789".charAt(Math.floor(16*Math.random()))})).join(""),null!==m&&(Ye=new k(m.userPermissions,m.userPassword,m.ownerPassword,V)),V};y.setFileId=function(t){return Y(t),this},y.getFileId=function(){return G()};var J=y.__private__.convertDateToPDFDate=function(t){var e=t.getTimezoneOffset(),r=e<0?"+":"-",n=Math.floor(Math.abs(e/60)),i=Math.abs(e%60),a=[r,Q(n),"'",Q(i),"'"].join("");return["D:",t.getFullYear(),Q(t.getMonth()+1),Q(t.getDate()),Q(t.getHours()),Q(t.getMinutes()),Q(t.getSeconds()),a].join("")},X=y.__private__.convertPDFDateToDate=function(t){var e=parseInt(t.substr(2,4),10),r=parseInt(t.substr(6,2),10)-1,n=parseInt(t.substr(8,2),10),i=parseInt(t.substr(10,2),10),a=parseInt(t.substr(12,2),10),o=parseInt(t.substr(14,2),10);return new Date(e,r,n,i,a,o,0)},K=y.__private__.setCreationDate=function(t){var e;if(void 0===t&&(t=new Date),t instanceof Date)e=J(t);else{if(!/^D:(20[0-2][0-9]|203[0-7]|19[7-9][0-9])(0[0-9]|1[0-2])([0-2][0-9]|3[0-1])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[1-5][0-9])(0[0-9]|[1-5][0-9])(\+0[0-9]|\+1[0-4]|-0[0-9]|-1[0-1])'(0[0-9]|[1-5][0-9])'?$/.test(t))throw new Error("Invalid argument passed to jsPDF.setCreationDate");e=t}return W=e},Z=y.__private__.getCreationDate=function(t){var e=W;return"jsDate"===t&&(e=X(W)),e};y.setCreationDate=function(t){return K(t),this},y.getCreationDate=function(t){return Z(t)};var $,Q=y.__private__.padd2=function(t){return("0"+parseInt(t)).slice(-2)},tt=y.__private__.padd2Hex=function(t){return("00"+(t=t.toString())).substr(t.length)},et=0,rt=[],nt=[],it=0,at=[],ot=[],st=!1,ct=nt,ut=function(){et=0,it=0,nt=[],rt=[],at=[],Qt=Kt(),te=Kt()};y.__private__.setCustomOutputDestination=function(t){st=!0,ct=t};var lt=function(t){st||(ct=t)};y.__private__.resetCustomOutputDestination=function(){st=!1,ct=nt};var ht=y.__private__.out=function(t){return t=t.toString(),it+=t.length+1,ct.push(t),ct},ft=y.__private__.write=function(t){return ht(1===arguments.length?t.toString():Array.prototype.join.call(arguments," "))},dt=y.__private__.getArrayBuffer=function(t){for(var e=t.length,r=new ArrayBuffer(e),n=new Uint8Array(r);e--;)n[e]=t.charCodeAt(e);return r},pt=[["Helvetica","helvetica","normal","WinAnsiEncoding"],["Helvetica-Bold","helvetica","bold","WinAnsiEncoding"],["Helvetica-Oblique","helvetica","italic","WinAnsiEncoding"],["Helvetica-BoldOblique","helvetica","bolditalic","WinAnsiEncoding"],["Courier","courier","normal","WinAnsiEncoding"],["Courier-Bold","courier","bold","WinAnsiEncoding"],["Courier-Oblique","courier","italic","WinAnsiEncoding"],["Courier-BoldOblique","courier","bolditalic","WinAnsiEncoding"],["Times-Roman","times","normal","WinAnsiEncoding"],["Times-Bold","times","bold","WinAnsiEncoding"],["Times-Italic","times","italic","WinAnsiEncoding"],["Times-BoldItalic","times","bolditalic","WinAnsiEncoding"],["ZapfDingbats","zapfdingbats","normal",null],["Symbol","symbol","normal",null]];y.__private__.getStandardFonts=function(){return pt};var gt=t.fontSize||16;y.__private__.setFontSize=y.setFontSize=function(t){return gt=S===x.ADVANCED?t/_t:t,this};var mt,vt=y.__private__.getFontSize=y.getFontSize=function(){return S===x.COMPAT?gt:gt*_t},bt=t.R2L||!1;y.__private__.setR2L=y.setR2L=function(t){return bt=t,this},y.__private__.getR2L=y.getR2L=function(){return bt};var yt,wt=y.__private__.setZoomMode=function(t){var e=[void 0,null,"fullwidth","fullheight","fullpage","original"];if(/^(?:\d+\.\d*|\d*\.\d+|\d+)%$/.test(t))mt=t;else if(isNaN(t)){if(-1===e.indexOf(t))throw new Error('zoom must be Integer (e.g. 2), a percentage Value (e.g. 300%) or fullwidth, fullheight, fullpage, original. "'+t+'" is not recognized.');mt=t}else mt=parseInt(t,10)};y.__private__.getZoomMode=function(){return mt};var Nt,Lt=y.__private__.setPageMode=function(t){if(-1==[void 0,null,"UseNone","UseOutlines","UseThumbs","FullScreen"].indexOf(t))throw new Error('Page mode must be one of UseNone, UseOutlines, UseThumbs, or FullScreen. "'+t+'" is not recognized.');yt=t};y.__private__.getPageMode=function(){return yt};var At=y.__private__.setLayoutMode=function(t){if(-1==[void 0,null,"continuous","single","twoleft","tworight","two"].indexOf(t))throw new Error('Layout mode must be one of continuous, single, twoleft, tworight. "'+t+'" is not recognized.');Nt=t};y.__private__.getLayoutMode=function(){return Nt},y.__private__.setDisplayMode=y.setDisplayMode=function(t,e,r){return wt(t),At(e),Lt(r),this};var xt={title:"",subject:"",author:"",keywords:"",creator:""};y.__private__.getDocumentProperty=function(t){if(-1===Object.keys(xt).indexOf(t))throw new Error("Invalid argument passed to jsPDF.getDocumentProperty");return xt[t]},y.__private__.getDocumentProperties=function(){return xt},y.__private__.setDocumentProperties=y.setProperties=y.setDocumentProperties=function(t){for(var e in xt)xt.hasOwnProperty(e)&&t[e]&&(xt[e]=t[e]);return this},y.__private__.setDocumentProperty=function(t,e){if(-1===Object.keys(xt).indexOf(t))throw new Error("Invalid arguments passed to jsPDF.setDocumentProperty");return xt[t]=e};var St,_t,Pt,kt,Ft,It={},Ct={},jt=[],Ot={},Bt={},Mt={},Et={},qt=null,Dt=0,Rt=[],Tt=new I(y),Ut=t.hotfixes||[],zt={},Ht={},Wt=[],Vt=function t(e,r,n,i,a,o){if(!(this instanceof t))return new t(e,r,n,i,a,o);isNaN(e)&&(e=1),isNaN(r)&&(r=0),isNaN(n)&&(n=0),isNaN(i)&&(i=1),isNaN(a)&&(a=0),isNaN(o)&&(o=0),this._matrix=[e,r,n,i,a,o]};Object.defineProperty(Vt.prototype,"sx",{get:function(){return this._matrix[0]},set:function(t){this._matrix[0]=t}}),Object.defineProperty(Vt.prototype,"shy",{get:function(){return this._matrix[1]},set:function(t){this._matrix[1]=t}}),Object.defineProperty(Vt.prototype,"shx",{get:function(){return this._matrix[2]},set:function(t){this._matrix[2]=t}}),Object.defineProperty(Vt.prototype,"sy",{get:function(){return this._matrix[3]},set:function(t){this._matrix[3]=t}}),Object.defineProperty(Vt.prototype,"tx",{get:function(){return this._matrix[4]},set:function(t){this._matrix[4]=t}}),Object.defineProperty(Vt.prototype,"ty",{get:function(){return this._matrix[5]},set:function(t){this._matrix[5]=t}}),Object.defineProperty(Vt.prototype,"a",{get:function(){return this._matrix[0]},set:function(t){this._matrix[0]=t}}),Object.defineProperty(Vt.prototype,"b",{get:function(){return this._matrix[1]},set:function(t){this._matrix[1]=t}}),Object.defineProperty(Vt.prototype,"c",{get:function(){return this._matrix[2]},set:function(t){this._matrix[2]=t}}),Object.defineProperty(Vt.prototype,"d",{get:function(){return this._matrix[3]},set:function(t){this._matrix[3]=t}}),Object.defineProperty(Vt.prototype,"e",{get:function(){return this._matrix[4]},set:function(t){this._matrix[4]=t}}),Object.defineProperty(Vt.prototype,"f",{get:function(){return this._matrix[5]},set:function(t){this._matrix[5]=t}}),Object.defineProperty(Vt.prototype,"rotation",{get:function(){return Math.atan2(this.shx,this.sx)}}),Object.defineProperty(Vt.prototype,"scaleX",{get:function(){return this.decompose().scale.sx}}),Object.defineProperty(Vt.prototype,"scaleY",{get:function(){return this.decompose().scale.sy}}),Object.defineProperty(Vt.prototype,"isIdentity",{get:function(){return 1===this.sx&&(0===this.shy&&(0===this.shx&&(1===this.sy&&(0===this.tx&&0===this.ty))))}}),Vt.prototype.join=function(t){return[this.sx,this.shy,this.shx,this.sy,this.tx,this.ty].map(E).join(t)},Vt.prototype.multiply=function(t){var e=t.sx*this.sx+t.shy*this.shx,r=t.sx*this.shy+t.shy*this.sy,n=t.shx*this.sx+t.sy*this.shx,i=t.shx*this.shy+t.sy*this.sy,a=t.tx*this.sx+t.ty*this.shx+this.tx,o=t.tx*this.shy+t.ty*this.sy+this.ty;return new Vt(e,r,n,i,a,o)},Vt.prototype.decompose=function(){var t=this.sx,e=this.shy,r=this.shx,n=this.sy,i=this.tx,a=this.ty,o=Math.sqrt(t*t+e*e),s=(t/=o)*r+(e/=o)*n;r-=t*s,n-=e*s;var c=Math.sqrt(r*r+n*n);return s/=c,t*(n/=c)>16&255,i=u>>8&255,a=255&u}if(void 0===i||void 0===o&&n===i&&i===a)if("string"==typeof n)r=n+" "+s[0];else switch(t.precision){case 2:r=R(n/255)+" "+s[0];break;case 3:default:r=T(n/255)+" "+s[0]}else if(void 0===o||"object"===e(o)){if(o&&!isNaN(o.a)&&0===o.a)return r=["1.","1.","1.",s[1]].join(" ");if("string"==typeof n)r=[n,i,a,s[1]].join(" ");else switch(t.precision){case 2:r=[R(n/255),R(i/255),R(a/255),s[1]].join(" ");break;default:case 3:r=[T(n/255),T(i/255),T(a/255),s[1]].join(" ")}}else if("string"==typeof n)r=[n,i,a,o,s[2]].join(" ");else switch(t.precision){case 2:r=[R(n),R(i),R(a),R(o),s[2]].join(" ");break;case 3:default:r=[T(n),T(i),T(a),T(o),s[2]].join(" ")}return r},ne=y.__private__.getFilters=function(){return f},ie=y.__private__.putStream=function(t){var e=(t=t||{}).data||"",r=t.filters||ne(),n=t.alreadyAppliedFilters||[],i=t.addLength1||!1,a=e.length,o=t.objectId,s=function(t){return t};if(null!==m&&void 0===o)throw new Error("ObjectId must be passed to putStream for file encryption");null!==m&&(s=Ye.encryptor(o,0));var c={};!0===r&&(r=["FlateEncode"]);var u=t.additionalKeyValues||[],l=(c=void 0!==M.API.processDataByFilters?M.API.processDataByFilters(e,r):{data:e,reverseChain:[]}).reverseChain+(Array.isArray(n)?n.join(" "):n.toString());if(0!==c.data.length&&(u.push({key:"Length",value:c.data.length}),!0===i&&u.push({key:"Length1",value:a})),0!=l.length)if(l.split("/").length-1==1)u.push({key:"Filter",value:l});else{u.push({key:"Filter",value:"["+l+"]"});for(var h=0;h>"),0!==c.data.length&&(ht("stream"),ht(s(c.data)),ht("endstream"))},ae=y.__private__.putPage=function(t){var e=t.number,r=t.data,n=t.objId,i=t.contentsObjId;Zt(n,!0),ht("<>"),ht("endobj");var a=r.join("\n");return S===x.ADVANCED&&(a+="\nQ"),Zt(i,!0),ie({data:a,filters:ne(),objectId:i}),ht("endobj"),n},oe=y.__private__.putPages=function(){var t,e,r=[];for(t=1;t<=Dt;t++)Rt[t].objId=Kt(),Rt[t].contentsObjId=Kt();for(t=1;t<=Dt;t++)r.push(ae({number:t,data:ot[t],objId:Rt[t].objId,contentsObjId:Rt[t].contentsObjId,mediaBox:Rt[t].mediaBox,cropBox:Rt[t].cropBox,bleedBox:Rt[t].bleedBox,trimBox:Rt[t].trimBox,artBox:Rt[t].artBox,userUnit:Rt[t].userUnit,rootDictionaryObjId:Qt,resourceDictionaryObjId:te}));Zt(Qt,!0),ht("<>"),ht("endobj"),Tt.publish("postPutPages")},se=function(t){Tt.publish("putFont",{font:t,out:ht,newObject:Xt,putStream:ie}),!0!==t.isAlreadyPutted&&(t.objectNumber=Xt(),ht("<<"),ht("/Type /Font"),ht("/BaseFont /"+F(t.postScriptName)),ht("/Subtype /Type1"),"string"==typeof t.encoding&&ht("/Encoding /"+t.encoding),ht("/FirstChar 32"),ht("/LastChar 255"),ht(">>"),ht("endobj"))},ce=function(){for(var t in It)It.hasOwnProperty(t)&&(!1===v||!0===v&&b.hasOwnProperty(t))&&se(It[t])},ue=function(t){t.objectNumber=Xt();var e=[];e.push({key:"Type",value:"/XObject"}),e.push({key:"Subtype",value:"/Form"}),e.push({key:"BBox",value:"["+[E(t.x),E(t.y),E(t.x+t.width),E(t.y+t.height)].join(" ")+"]"}),e.push({key:"Matrix",value:"["+t.matrix.toString()+"]"});var r=t.pages[1].join("\n");ie({data:r,additionalKeyValues:e,objectId:t.objectNumber}),ht("endobj")},le=function(){for(var t in zt)zt.hasOwnProperty(t)&&ue(zt[t])},he=function(t,e){var r,n=[],i=1/(e-1);for(r=0;r<1;r+=i)n.push(r);if(n.push(1),0!=t[0].offset){var a={offset:0,color:t[0].color};t.unshift(a)}if(1!=t[t.length-1].offset){var o={offset:1,color:t[t.length-1].color};t.push(o)}for(var s="",c=0,u=0;ut[c+1].offset;)c++;var l=t[c].offset,h=(r-l)/(t[c+1].offset-l),f=t[c].color,d=t[c+1].color;s+=tt(Math.round((1-h)*f[0]+h*d[0]).toString(16))+tt(Math.round((1-h)*f[1]+h*d[1]).toString(16))+tt(Math.round((1-h)*f[2]+h*d[2]).toString(16))}return s.trim()},fe=function(t,e){e||(e=21);var r=Xt(),n=he(t.colors,e),i=[];i.push({key:"FunctionType",value:"0"}),i.push({key:"Domain",value:"[0.0 1.0]"}),i.push({key:"Size",value:"["+e+"]"}),i.push({key:"BitsPerSample",value:"8"}),i.push({key:"Range",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),i.push({key:"Decode",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),ie({data:n,additionalKeyValues:i,alreadyAppliedFilters:["/ASCIIHexDecode"],objectId:r}),ht("endobj"),t.objectNumber=Xt(),ht("<< /ShadingType "+t.type),ht("/ColorSpace /DeviceRGB");var a="/Coords ["+E(parseFloat(t.coords[0]))+" "+E(parseFloat(t.coords[1]))+" ";2===t.type?a+=E(parseFloat(t.coords[2]))+" "+E(parseFloat(t.coords[3])):a+=E(parseFloat(t.coords[2]))+" "+E(parseFloat(t.coords[3]))+" "+E(parseFloat(t.coords[4]))+" "+E(parseFloat(t.coords[5])),ht(a+="]"),t.matrix&&ht("/Matrix ["+t.matrix.toString()+"]"),ht("/Function "+r+" 0 R"),ht("/Extend [true true]"),ht(">>"),ht("endobj")},de=function(t,e){var r=Kt(),n=Xt();e.push({resourcesOid:r,objectOid:n}),t.objectNumber=n;var i=[];i.push({key:"Type",value:"/Pattern"}),i.push({key:"PatternType",value:"1"}),i.push({key:"PaintType",value:"1"}),i.push({key:"TilingType",value:"1"}),i.push({key:"BBox",value:"["+t.boundingBox.map(E).join(" ")+"]"}),i.push({key:"XStep",value:E(t.xStep)}),i.push({key:"YStep",value:E(t.yStep)}),i.push({key:"Resources",value:r+" 0 R"}),t.matrix&&i.push({key:"Matrix",value:"["+t.matrix.toString()+"]"}),ie({data:t.stream,additionalKeyValues:i,objectId:t.objectNumber}),ht("endobj")},pe=function(t){var e;for(e in Ot)Ot.hasOwnProperty(e)&&(Ot[e]instanceof O?fe(Ot[e]):Ot[e]instanceof B&&de(Ot[e],t))},ge=function(t){for(var e in t.objectNumber=Xt(),ht("<<"),t)switch(e){case"opacity":ht("/ca "+R(t[e]));break;case"stroke-opacity":ht("/CA "+R(t[e]))}ht(">>"),ht("endobj")},me=function(){var t;for(t in Mt)Mt.hasOwnProperty(t)&&ge(Mt[t])},ve=function(){for(var t in ht("/XObject <<"),zt)zt.hasOwnProperty(t)&&zt[t].objectNumber>=0&&ht("/"+t+" "+zt[t].objectNumber+" 0 R");Tt.publish("putXobjectDict"),ht(">>")},be=function(){Ye.oid=Xt(),ht("<<"),ht("/Filter /Standard"),ht("/V "+Ye.v),ht("/R "+Ye.r),ht("/U <"+Ye.toHexString(Ye.U)+">"),ht("/O <"+Ye.toHexString(Ye.O)+">"),ht("/P "+Ye.P),ht(">>"),ht("endobj")},ye=function(){for(var t in ht("/Font <<"),It)It.hasOwnProperty(t)&&(!1===v||!0===v&&b.hasOwnProperty(t))&&ht("/"+t+" "+It[t].objectNumber+" 0 R");ht(">>")},we=function(){if(Object.keys(Ot).length>0){for(var t in ht("/Shading <<"),Ot)Ot.hasOwnProperty(t)&&Ot[t]instanceof O&&Ot[t].objectNumber>=0&&ht("/"+t+" "+Ot[t].objectNumber+" 0 R");Tt.publish("putShadingPatternDict"),ht(">>")}},Ne=function(t){if(Object.keys(Ot).length>0){for(var e in ht("/Pattern <<"),Ot)Ot.hasOwnProperty(e)&&Ot[e]instanceof y.TilingPattern&&Ot[e].objectNumber>=0&&Ot[e].objectNumber>")}},Le=function(){if(Object.keys(Mt).length>0){var t;for(t in ht("/ExtGState <<"),Mt)Mt.hasOwnProperty(t)&&Mt[t].objectNumber>=0&&ht("/"+t+" "+Mt[t].objectNumber+" 0 R");Tt.publish("putGStateDict"),ht(">>")}},Ae=function(t){Zt(t.resourcesOid,!0),ht("<<"),ht("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"),ye(),we(),Ne(t.objectOid),Le(),ve(),ht(">>"),ht("endobj")},xe=function(){var t=[];ce(),me(),le(),pe(t),Tt.publish("putResources"),t.forEach(Ae),Ae({resourcesOid:te,objectOid:Number.MAX_SAFE_INTEGER}),Tt.publish("postPutResources")},Se=function(){Tt.publish("putAdditionalObjects");for(var t=0;t>8&&(c=!0);t=s.join("")}for(r=t.length;void 0===c&&0!==r;)t.charCodeAt(r-1)>>8&&(c=!0),r--;if(!c)return t;for(s=e.noBOM?[]:[254,255],r=0,n=t.length;r>8)>>8)throw new Error("Character at position "+r+" of string '"+t+"' exceeds 16bits. Cannot be encoded into UCS-2 BE");s.push(l),s.push(u-(l<<8))}return String.fromCharCode.apply(void 0,s)},Ce=y.__private__.pdfEscape=y.pdfEscape=function(t,e){return Ie(t,e).replace(/\\/g,"\\\\").replace(/\(/g,"\\(").replace(/\)/g,"\\)")},je=y.__private__.beginPage=function(t){ot[++Dt]=[],Rt[Dt]={objId:0,contentsObjId:0,userUnit:Number(d),artBox:null,bleedBox:null,cropBox:null,trimBox:null,mediaBox:{bottomLeftX:0,bottomLeftY:0,topRightX:Number(t[0]),topRightY:Number(t[1])}},Me(Dt),lt(ot[$])},Oe=function(t,e){var r,n,o;switch(a=e||a,"string"==typeof t&&(r=A(t.toLowerCase()),Array.isArray(r)&&(n=r[0],o=r[1])),Array.isArray(t)&&(n=t[0]*_t,o=t[1]*_t),isNaN(n)&&(n=s[0],o=s[1]),(n>14400||o>14400)&&(i.warn("A page in a PDF can not be wider or taller than 14400 userUnit. jsPDF limits the width/height to 14400"),n=Math.min(14400,n),o=Math.min(14400,o)),s=[n,o],a.substr(0,1)){case"l":o>n&&(s=[o,n]);break;case"p":n>o&&(s=[o,n])}je(s),pr(fr),ht(Lr),0!==kr&&ht(kr+" J"),0!==Fr&&ht(Fr+" j"),Tt.publish("addPage",{pageNumber:Dt})},Be=function(t){t>0&&t<=Dt&&(ot.splice(t,1),Rt.splice(t,1),Dt--,$>Dt&&($=Dt),this.setPage($))},Me=function(t){t>0&&t<=Dt&&($=t)},Ee=y.__private__.getNumberOfPages=y.getNumberOfPages=function(){return ot.length-1},qe=function(t,e,r){var n,a=void 0;return r=r||{},t=void 0!==t?t:It[St].fontName,e=void 0!==e?e:It[St].fontStyle,n=t.toLowerCase(),void 0!==Ct[n]&&void 0!==Ct[n][e]?a=Ct[n][e]:void 0!==Ct[t]&&void 0!==Ct[t][e]?a=Ct[t][e]:!1===r.disableWarning&&i.warn("Unable to look up font label for font '"+t+"', '"+e+"'. Refer to getFontList() for available fonts."),a||r.noFallback||null==(a=Ct.times[e])&&(a=Ct.times.normal),a},De=y.__private__.putInfo=function(){var t=Xt(),e=function(t){return t};for(var r in null!==m&&(e=Ye.encryptor(t,0)),ht("<<"),ht("/Producer ("+Ce(e("jsPDF "+M.version))+")"),xt)xt.hasOwnProperty(r)&&xt[r]&&ht("/"+r.substr(0,1).toUpperCase()+r.substr(1)+" ("+Ce(e(xt[r]))+")");ht("/CreationDate ("+Ce(e(W))+")"),ht(">>"),ht("endobj")},Re=y.__private__.putCatalog=function(t){var e=(t=t||{}).rootDictionaryObjId||Qt;switch(Xt(),ht("<<"),ht("/Type /Catalog"),ht("/Pages "+e+" 0 R"),mt||(mt="fullwidth"),mt){case"fullwidth":ht("/OpenAction [3 0 R /FitH null]");break;case"fullheight":ht("/OpenAction [3 0 R /FitV null]");break;case"fullpage":ht("/OpenAction [3 0 R /Fit]");break;case"original":ht("/OpenAction [3 0 R /XYZ null null 1]");break;default:var r=""+mt;"%"===r.substr(r.length-1)&&(mt=parseInt(mt)/100),"number"==typeof mt&&ht("/OpenAction [3 0 R /XYZ null null "+R(mt)+"]")}switch(Nt||(Nt="continuous"),Nt){case"continuous":ht("/PageLayout /OneColumn");break;case"single":ht("/PageLayout /SinglePage");break;case"two":case"twoleft":ht("/PageLayout /TwoColumnLeft");break;case"tworight":ht("/PageLayout /TwoColumnRight")}yt&&ht("/PageMode /"+yt),Tt.publish("putCatalog"),ht(">>"),ht("endobj")},Te=y.__private__.putTrailer=function(){ht("trailer"),ht("<<"),ht("/Size "+(et+1)),ht("/Root "+et+" 0 R"),ht("/Info "+(et-1)+" 0 R"),null!==m&&ht("/Encrypt "+Ye.oid+" 0 R"),ht("/ID [ <"+V+"> <"+V+"> ]"),ht(">>")},Ue=y.__private__.putHeader=function(){ht("%PDF-"+w),ht("%ºß¬à")},ze=y.__private__.putXRef=function(){var t="0000000000";ht("xref"),ht("0 "+(et+1)),ht("0000000000 65535 f ");for(var e=1;e<=et;e++){"function"==typeof rt[e]?ht((t+rt[e]()).slice(-10)+" 00000 n "):void 0!==rt[e]?ht((t+rt[e]).slice(-10)+" 00000 n "):ht("0000000000 00000 n ")}},He=y.__private__.buildDocument=function(){ut(),lt(nt),Tt.publish("buildDocument"),Ue(),oe(),Se(),xe(),null!==m&&be(),De(),Re();var t=it;return ze(),Te(),ht("startxref"),ht(""+t),ht("%%EOF"),lt(ot[$]),nt.join("\n")},We=y.__private__.getBlob=function(t){return new Blob([dt(t)],{type:"application/pdf"})},Ve=y.output=y.__private__.output=Fe((function(t,e){switch("string"==typeof(e=e||{})?e={filename:e}:e.filename=e.filename||"generated.pdf",t){case void 0:return He();case"save":y.save(e.filename);break;case"arraybuffer":return dt(He());case"blob":return We(He());case"bloburi":case"bloburl":if(void 0!==r.URL&&"function"==typeof r.URL.createObjectURL)return r.URL&&r.URL.createObjectURL(We(He()))||void 0;i.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");break;case"datauristring":case"dataurlstring":var n="",a=He();try{n=u(a)}catch(t){n=u(unescape(encodeURIComponent(a)))}return"data:application/pdf;filename="+e.filename+";base64,"+n;case"pdfobjectnewwindow":if("[object Window]"===Object.prototype.toString.call(r)){var o="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js",s=' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';e.pdfObjectUrl&&(o=e.pdfObjectUrl,s="");var c=' - - - - diff --git a/lib/platform/web.js b/lib/platform/web.js deleted file mode 100644 index 7bdb5dc..0000000 --- a/lib/platform/web.js +++ /dev/null @@ -1,341 +0,0 @@ -/** - * 平台抽象层 - 网页实现 - * 使用 LocalStorage、File Picker、Blob 下载、navigator 等 Web API。 - */ -(function () { - 'use strict'; - - var HISTORY_KEY = 'soondesign_history'; - var VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '3.2.101'; - - /** 站点目录前缀(支持部署在子目录 / 宝塔子路径):去掉入口 HTML 文件名 */ - function getAppBasePathname() { - var p = (typeof window !== 'undefined' && window.location && window.location.pathname) ? window.location.pathname : '/'; - // Nginx/宝塔可能把页面写成 /design2.web.html/ ,末尾斜杠会导致匹配不到 .html,误判站点根为「目录 URL」→ 回首页 404 - while (p.length > 1 && p.endsWith('/')) { - p = p.slice(0, -1); - } - if (/\.web\.html$/i.test(p) || /\.html$/i.test(p)) { - p = p.replace(/[^/]+$/, ''); - } - if (!p.endsWith('/')) p += '/'; - return p; - } - - // API 相对站点根目录(与 api/README 中「相对网站根目录的 /api/」一致) - var SERVER_API_BASE = (typeof window !== 'undefined' && window.location) - ? (window.location.origin + getAppBasePathname() + 'api/') - : '/api/'; - - function navigateToPage(pathWithQuery) { - try { - location.href = new URL(pathWithQuery, window.location.href).href; - } catch (e) { - location.href = pathWithQuery; - } - } - - function getLocale() { - var lang = typeof navigator !== 'undefined' ? (navigator.language || navigator.browserLanguage || '') : ''; - if (lang.indexOf('zh') === 0) return lang.indexOf('TW') >= 0 ? 'ozh' : 'zh'; - return 'en'; - } - - function getSystemFonts() { - var list = [ - 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia', - 'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong', - 'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB' - ]; - if (typeof document !== 'undefined' && document.fonts && document.fonts.forEach) { - var set = {}; - document.fonts.forEach(function (f) { - var name = (f.family || '').replace(/^["']|["']$/g, ''); - if (name) set[name] = 1; - }); - list = Object.keys(set).length ? Object.keys(set).sort() : list; - } - return Promise.resolve(list); - } - - var bridge = { - readHistory: function () { - try { - var raw = localStorage.getItem(HISTORY_KEY); - return Promise.resolve(raw ? JSON.parse(raw) : { history: [] }); - } catch (e) { - return Promise.resolve({ history: [] }); - } - }, - writeHistory: function (data) { - try { - localStorage.setItem(HISTORY_KEY, JSON.stringify(data)); - return Promise.resolve({ success: true }); - } catch (e) { - return Promise.resolve({ success: false, error: e.message }); - } - }, - readJsonFile: function (pathOrHandle) { - if (!pathOrHandle) return Promise.resolve(null); - if (typeof pathOrHandle === 'object' && pathOrHandle.text) { - return pathOrHandle.text().then(function (t) { - try { return JSON.parse(t); } catch (e) { return null; } - }); - } - var key = typeof pathOrHandle === 'string' ? pathOrHandle : ''; - if (key.indexOf('soondesign_session:') === 0) { - try { - var j = sessionStorage.getItem(key); - if (!j && typeof localStorage !== 'undefined') j = localStorage.getItem(key); - return Promise.resolve(j ? JSON.parse(j) : null); - } catch (e) { return Promise.resolve(null); } - } - // 服务器文件:从服务器读取 - if (key && key.indexOf('.soon') > 0 && typeof fetch !== 'undefined') { - var isServerEnv = typeof window !== 'undefined' && window.location && - (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1' && window.location.protocol !== 'file:'); - if (isServerEnv) { - return fetch(SERVER_API_BASE + 'read_file.php?fileName=' + encodeURIComponent(key)) - .then(function(response) { - if (!response.ok) return null; - return response.json(); - }) - .catch(function() { return null; }); - } - } - return Promise.resolve(null); - }, - showOpenDialog: function (options) { - return new Promise(function (resolve) { - var input = document.createElement('input'); - input.type = 'file'; - // 根据 options.filters 动态设置 accept - if (options && options.filters && Array.isArray(options.filters) && options.filters.length > 0) { - var acceptList = []; - options.filters.forEach(function(filter) { - if (filter.extensions && Array.isArray(filter.extensions)) { - filter.extensions.forEach(function(ext) { - // 移除点号(如果有),然后添加点号前缀 - var cleanExt = ext.replace(/^\./, ''); - // 图片类型使用 image/* MIME 类型,其他使用扩展名 - if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].indexOf(cleanExt.toLowerCase()) >= 0) { - var mimeType = 'image/' + (cleanExt.toLowerCase() === 'jpg' ? 'jpeg' : cleanExt.toLowerCase()); - if (acceptList.indexOf(mimeType) < 0) acceptList.push(mimeType); - } else { - var extWithDot = '.' + cleanExt; - if (acceptList.indexOf(extWithDot) < 0) acceptList.push(extWithDot); - } - }); - } - }); - input.accept = acceptList.length > 0 ? acceptList.join(',') : ''; - } else { - // 默认:.soon 文件 - input.accept = '.soon,application/json'; - } - input.style.display = 'none'; - input.onchange = function () { - var f = input.files && input.files[0]; - document.body.removeChild(input); - if (!f) resolve({ canceled: true }); - else resolve({ canceled: false, filePaths: [], files: [f], file: f }); - }; - document.body.appendChild(input); - input.click(); - }); - }, - showSaveDialog: function (options) { - var raw = (options && options.defaultPath) ? options.defaultPath : ''; - var defaultName = raw - ? (raw.indexOf('soondesign_session:') === 0 ? raw.substring('soondesign_session:'.length) : raw.split(/[/\\]/).pop()) - : 'design.soon'; - return new Promise(function (resolve) { - if (typeof window.showSaveFilePicker !== 'function') { - var name = typeof prompt === 'function' ? prompt('保存为文件名(如 xxx.soon)', defaultName) : defaultName; - if (name === null) { - resolve({ canceled: true }); - return; - } - var fp = (name && String(name).trim()) ? String(name).trim() : defaultName; - if (fp.indexOf('.soon') === -1 || fp.slice(-5).toLowerCase() !== '.soon') { - fp = (fp.replace(/\.soon$/i, '') || 'design') + '.soon'; - } - resolve({ canceled: false, filePath: fp, useDownload: true }); - return; - } - window.showSaveFilePicker({ - suggestedName: defaultName, - types: [{ description: 'SoonDesign', accept: { 'application/json': ['.soon'] } }] - }).then(function (handle) { - var filePath = (handle && handle.name) ? handle.name : defaultName; - resolve({ canceled: false, filePath: filePath, fileHandle: handle }); - }).catch(function () { - resolve({ canceled: true }); - }); - }); - }, - writeFile: function (pathOrHandle, content) { - var isBlob = content instanceof Blob; - var str = typeof content === 'string' ? content : (isBlob ? null : (content && content.toString ? content.toString() : '')); - // File System Access API(浏览器原生文件保存) - if (pathOrHandle && pathOrHandle.createWritable) { - return pathOrHandle.createWritable().then(function (w) { - if (isBlob) w.write(content); - else w.write(str); - return w.close(); - }); - } - // 服务器环境:上传到服务器 - var name = typeof pathOrHandle === 'string' ? pathOrHandle : (isBlob ? 'output.pdf' : 'design.soon'); - var isServerEnv = typeof window !== 'undefined' && window.location && - (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1' && window.location.protocol !== 'file:'); - - if (isServerEnv && !isBlob && typeof fetch !== 'undefined') { - // 上传到服务器 - var formData = new FormData(); - formData.append('fileName', name); - formData.append('fileContent', str); - - return fetch(SERVER_API_BASE + 'save_file.php', { - method: 'POST', - body: formData - }).then(function(response) { - return response.json(); - }).then(function(result) { - if (result.success) { - return Promise.resolve(); - } else { - throw new Error(result.error || '保存失败'); - } - }).catch(function(err) { - // 如果服务器保存失败,降级为下载 - console.warn('服务器保存失败,降级为下载:', err); - var a = document.createElement('a'); - a.download = name; - a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(str); - a.click(); - return Promise.resolve(); - }); - } - // 本地环境或 Blob:触发下载 - var a = document.createElement('a'); - a.download = name; - if (isBlob) a.href = URL.createObjectURL(content); - else a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(str); - a.click(); - if (isBlob && a.href) URL.revokeObjectURL(a.href); - return Promise.resolve(); - }, - readFile: function (pathOrHandle) { - if (pathOrHandle && pathOrHandle.getFile) { - return pathOrHandle.getFile().then(function (f) { - return new Promise(function (res, rej) { - var r = new FileReader(); - r.onload = function () { res(r.result); }; - r.onerror = rej; - r.readAsArrayBuffer(f); - }); - }); - } - if (pathOrHandle && pathOrHandle.arrayBuffer) { - return pathOrHandle.arrayBuffer(); - } - return Promise.reject(new Error('No file')); - }, - getAppVersion: function () { return Promise.resolve(VERSION); }, - getLocale: getLocale, - getUserDataPath: function () { return ''; }, - getSystemFonts: getSystemFonts, - openDesignPage: function (file, type) { - var t = type || 1; - if (file && typeof sessionStorage !== 'undefined') { - try { - sessionStorage.setItem('soondesign_open_file', file); - sessionStorage.setItem('soondesign_open_type', String(t)); - } catch (e) {} - } - var fileParam = file ? encodeURIComponent(file) : ''; - navigateToPage('design' + t + '.web.html?file=' + fileParam + '&type=' + t); - }, - openFirstPage: function () { - var loc = typeof window !== 'undefined' ? window.location : null; - if (!loc) return; - // 显式拼站点根(与 getAppBasePathname 一致),避免「路径末尾斜杠 / URL API」与 navigateToPage('.') 在个别环境下的解析差异 - loc.href = loc.origin + getAppBasePathname(); - }, - onClose: function (callback) { - if (typeof window !== 'undefined') { - window.addEventListener('beforeunload', callback); - } - }, - runClose: function () { - if (typeof window !== 'undefined' && window.close) window.close(); - }, - openHelp: function () { - try { - window.open(new URL('help/User Manual.pdf', window.location.href).href, '_blank'); - } catch (e) { - window.open('help/User Manual.pdf', '_blank'); - } - }, - printPdf: function (urlOrBlob) { - var url = urlOrBlob; - if (urlOrBlob && typeof urlOrBlob === 'object' && !(urlOrBlob instanceof String)) { - url = URL.createObjectURL(urlOrBlob); - } - var w = window.open(url, '_blank'); - if (w) w.onload = function () { w.print(); }; - }, - getScaleRate: function () { - return typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1; - }, - clipboard: { - readText: function () { - return navigator.clipboard && navigator.clipboard.readText ? navigator.clipboard.readText() : Promise.resolve(''); - }, - writeText: function (text) { - if (navigator.clipboard && navigator.clipboard.writeText) { - return navigator.clipboard.writeText(text); - } - return Promise.resolve(); - } - } - }; - - window.platformBridge = bridge; - - var pathStub = { - join: function () { return [].slice.call(arguments).join('/').replace(/\/+/g, '/'); } - }; - - window.sysAPI = { - readHistory: bridge.readHistory, - writeHistory: bridge.writeHistory, - readJsonFile: bridge.readJsonFile, - getAppVersion: bridge.getAppVersion - }; - window.dialog = { - showOpenDialog: bridge.showOpenDialog, - showSaveDialog: bridge.showSaveDialog - }; - window.path = pathStub; - window.fs = null; - window.remote = null; - var loc = getLocale(); - window.ipcRenderer = { - send: function (ch, a1, a2) { - if (ch === 'get-sys-language') { window._sysLanPending = true; setTimeout(function () { if (window._sysLanCb && window._sysLanPending) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); } - if (ch === 'open-first-page' && bridge.openFirstPage) { bridge.openFirstPage(); } - if (ch === 'open-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); } - if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); } - if (ch === 'run-close' && bridge.runClose) { bridge.runClose(); } - }, - on: function (ch, cb) { - if (ch === 'sys-lan') { window._sysLanCb = cb; if (window._sysLanPending) setTimeout(function () { if (window._sysLanCb) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); } - if (ch === 'close') window._closeCb = cb; - } - }; - window.clipboard = bridge.clipboard; - window.exePath = ''; - window.fullPath = ''; -})(); diff --git a/output.pdf b/output.pdf deleted file mode 100644 index 76cec24..0000000 Binary files a/output.pdf and /dev/null differ diff --git a/package copy.json b/package copy.json deleted file mode 100644 index c5f8a00..0000000 --- a/package copy.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "soondesign", - "version": "2.1.248", - "description": "tools of design soon files", - "main": "main.js", - "scripts": { - "start": "electron .", - "package": "electron-packager ./ SoonDesign --out ./OutApp --platform=win32 --arch=x64 --electron-version 15.1.1 --icon public/images/favicon.ico --asar --overwrite --download.mirrorOptions.mirror=https://npmmirror.com/mirrors/electron/", - "dist": "webpack", - "build": "electron-builder" - }, - "electronPackagerConfig": { - "packageManager": "npm", - "icon": "public/images/favicon.ico" - }, - "build": { - "productName": "SoonDesign", - "appId": "com.SoonDesign", - "copyright": "Copyright@2023 cardsoon.com", - "directories": { - "output": "build" - }, - "nsis": { - "oneClick": false, - "allowElevation": true, - "allowToChangeInstallationDirectory": true, - "installerIcon": "./public/images/favicon.ico", - "uninstallerIcon": "./public/images/favicon.ico", - "installerHeaderIcon": "./public/images/favicon.ico", - "createDesktopShortcut": true, - "createStartMenuShortcut": true - }, - "fileAssociations": [ - { - "name": "SoonDesign file", - "ext": "soon", - "icon": "./public/images/favicon.ico", - "description": "SoonDesign file associations" - } - ], - "asar": true, - "win": { - "icon": "public/images/favicon.ico", - "requestedExecutionLevel": "requireAdministrator", - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ] - } - }, - "repository": "", - "keywords": [ - "Electron", - "quick", - "start", - "tutorial", - "demo" - ], - "author": "", - "license": "MIT", - "devDependencies": { - "electron": "^15.2.0", - "electron-builder": "^23.3.3", - "electron-packager": "^15.5.2" - }, - "dependencies": { - "@electron/remote": "^2.0.8", - "buffer": "^6.0.3", - "bytenode": "^1.3.4", - "font-list": "^1.4.2", - "jr-qrcode": "^1.1.4", - "jsbarcode": "^3.11.5", - "less": "^4.1.2", - "less-loader": "^10.2.0", - "save": "^2.9.0" - } -} diff --git a/package1.json b/package1.json deleted file mode 100644 index d7677d5..0000000 --- a/package1.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "name": "SoonDesign", - "version": "2.3.017", - "description": "tools of design soon files", - "main": "main.js", - "scripts": { - "start": "electron .", - "encrypt:win": "scripts/encrypt/encrypt-electron-win.bat", - "encrypt:linux": "./scripts/encrypt/encrypt-electron.sh", - "cleanup:jsc": "node scripts/encrypt/cleanup-jsc.js", - "cleanup": "node scripts/encrypt/cleanup.js", - "restore": "node scripts/encrypt/restore.js", - "build:win": "electron-builder --win --x64", - "build:mac": "electron-builder --mac --x64", - "build:linux": "electron-builder --linux --x64", - "build:linux:arm64": "electron-builder --linux --arm64", - "build:all": "electron-builder -mwl", - "release:win": "yarn encrypt:win && yarn cleanup && yarn build:win", - "release:mac": "yarn encrypt:linux && yarn cleanup && yarn build:mac", - "release:linux": "yarn encrypt:linux && yarn cleanup && yarn build:linux", - "release:linux:arm64": "yarn build:linux:arm64" - }, - "build": { - "productName": "SoonDesign", - "appId": "com.SoonDesign", - "copyright": "", - "directories": { - "output": "build" - }, - "extraFiles": [ - "lib" - ], - "files": [ - "**/*", - "!.backup/**", - "!scripts/**", - "!**/*.jsc.map", - "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}", - "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}", - "!**/node_modules/*.d.ts", - "!**/node_modules/.bin", - "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}", - "!.editorconfig", - "!**/._*", - "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}", - "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}", - "!**/{appveyor.yml,.travis.yml,circle.yml}", - "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}" - ], - "nsis": { - "oneClick": false, - "allowElevation": true, - "allowToChangeInstallationDirectory": true, - "installerIcon": "public/images/favicon.ico", - "uninstallerIcon": "public/images/favicon.ico", - "installerHeaderIcon": "public/images/favicon.ico", - "createDesktopShortcut": true, - "createStartMenuShortcut": true - }, - "fileAssociations": [ - { - "name": "SoonDesign file", - "ext": "soon", - "icon": "public/images/favicon.ico", - "description": "SoonDesign file associations" - } - ], - "asar": true, - "asarUnpack": [ - "lib/**" - ], - "win": { - "icon": "public/images/favicon.ico", - "requestedExecutionLevel": "asInvoker", - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ] - }, - "mac": { - "target": [ - { "target": "dmg", "arch": ["x64"] }, - { "target": "zip", "arch": ["x64"] } - ], - "category": "public.app-category.utilities", - "icon": "public/images/favicon.icns" - }, - "linux": { - "target": [ - { "target": "AppImage", "arch": ["x64", "arm64"] }, - { "target": "deb", "arch": ["x64", "arm64"] } - ], - "category": "Utility", - "icon": "public/images/IconA256.png", - "desktop": { - "Name": "SoonDesign", - "Comment": "tools of design soon files" - } - } - }, - "electronPackagerConfig": { - "packageManager": "yarn", - "icon": "./public/images/favicon.ico" - }, - - "repository": "", - "keywords": [ - "Electron", - "quick", - "start", - "tutorial", - "demo" - ], - "author": "", - "license": "MIT", - "devDependencies": { - "electron": "^25.0.0", - "electron-builder": "^23.3.3" - }, - "dependencies": { - "@electron/remote": "^2.0.8", - "buffer": "^6.0.3", - "bytenode": "^1.3.4", - "electron-squirrel-startup": "^1.0.1", - "font-list": "^1.4.2", - "jr-qrcode": "^1.1.4", - "jsbarcode": "^3.11.5", - "less": "^4.1.2", - "less-loader": "^10.2.0", - "save": "^2.9.0" - } -} diff --git a/print.png b/print.png deleted file mode 100644 index da9159c..0000000 Binary files a/print.png and /dev/null differ diff --git a/public/images/ps516E.tmp b/public/images/ps516E.tmp deleted file mode 100644 index e69de29..0000000 diff --git a/public/js/fabric.min.js.bak b/public/js/fabric.min.js.bak deleted file mode 100644 index 9fca391..0000000 --- a/public/js/fabric.min.js.bak +++ /dev/null @@ -1 +0,0 @@ -var fabric=fabric||{version:"4.6.0"};if("undefined"!=typeof exports?exports.fabric=fabric:"function"==typeof define&&define.amd&&define([],function(){return fabric}),"undefined"!=typeof document&&"undefined"!=typeof window)document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?fabric.document=document:fabric.document=document.implementation.createHTMLDocument(""),fabric.window=window;else{var jsdom=require("jsdom"),virtualWindow=new jsdom.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window;fabric.document=virtualWindow.document,fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=virtualWindow,DOMParser=fabric.window.DOMParser}function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,s=t.destinationHeight;i===n&&r===s||(e.width=n,e.height=s)}function copyGLTo2DDrawImage(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var s=i.height-r.height;n.drawImage(i,0,s,r.width,r.height,0,0,r.width,r.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,n=e.destinationHeight,s=r*n*4,o=new Uint8Array(this.imageBuffer,0,s),a=new Uint8ClampedArray(this.imageBuffer,0,s);t.readPixels(0,0,r,n,t.RGBA,t.UNSIGNED_BYTE,o);var c=new ImageData(a,r,n);i.putImageData(c,0,0)}fabric.isTouchSupported="ontouchstart"in fabric.window||"ontouchstart"in fabric.document||fabric.window&&fabric.window.navigator&&0_)for(var C=1,S=d.length;Ct[i-2].x?1:n.x===t[i-2].x?0:-1,c=n.y>t[i-2].y?1:n.y===t[i-2].y?0:-1),r.push(["L",n.x+a*e,n.y+c*e]),r},fabric.util.getPathSegmentsInfo=l,fabric.util.getBoundsOfCurve=m,fabric.util.getPointOnPath=function(t,e,i){i||(i=l(t));for(var r=0;0/g,">")},graphemeSplit:function(t){var e,i=0,r=[];for(i=0;it.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,e){return void 0===e&&(e=.5),e=Math.max(Math.min(1,e),0),new i(this.x+(t.x-this.x)*e,this.y+(t.y-this.y)*e)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new i(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new i(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new i(this.x,this.y)}}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var f=t.fabric||(t.fabric={});function d(t){this.status=t,this.points=[]}f.Intersection?f.warn("fabric.Intersection is already defined"):(f.Intersection=d,f.Intersection.prototype={constructor:d,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},f.Intersection.intersectLineLine=function(t,e,i,r){var n,s=(r.x-i.x)*(t.y-i.y)-(r.y-i.y)*(t.x-i.x),o=(e.x-t.x)*(t.y-i.y)-(e.y-t.y)*(t.x-i.x),a=(r.y-i.y)*(e.x-t.x)-(r.x-i.x)*(e.y-t.y);if(0!==a){var c=s/a,h=o/a;0<=c&&c<=1&&0<=h&&h<=1?(n=new d("Intersection")).appendPoint(new f.Point(t.x+c*(e.x-t.x),t.y+c*(e.y-t.y))):n=new d}else n=new d(0===s||0===o?"Coincident":"Parallel");return n},f.Intersection.intersectLinePolygon=function(t,e,i){var r,n,s,o,a=new d,c=i.length;for(o=0;o=c&&(h.x-=c),h.x<=-c&&(h.x+=c),h.y>=c&&(h.y-=c),h.y<=c&&(h.y+=c),h.x-=o.offsetX,h.y-=o.offsetY,h}function y(t){return t.flipX!==t.flipY}function _(t,e,i,r,n){if(0!==t[e]){var s=n/t._getTransformedDimensions()[r]*t[i];t.set(i,s)}}function x(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(0,s.skewY),a=P(e,e.originX,e.originY,i,r),c=Math.abs(2*a.x)-o.x,h=s.skewX;c<2?n=0:(n=v(Math.atan2(c/s.scaleX,o.y/s.scaleY)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),y(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().y;s.set("skewX",n),_(s,"skewY","scaleY","y",u)}return l}function C(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(s.skewX,0),a=P(e,e.originX,e.originY,i,r),c=Math.abs(2*a.y)-o.y,h=s.skewY;c<2?n=0:(n=v(Math.atan2(c/s.scaleY,o.x/s.scaleX)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),y(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().x;s.set("skewY",n),_(s,"skewX","scaleX","x",u)}return l}function E(t,e,i,r,n){n=n||{};var s,o,a,c,h,l,u=e.target,f=u.lockScalingX,d=u.lockScalingY,g=n.by,p=w(t,u),v=k(u,g,p),m=e.gestureScale;if(v)return!1;if(m)o=e.scaleX*m,a=e.scaleY*m;else{if(s=P(e,e.originX,e.originY,i,r),h="y"!==g?T(s.x):1,l="x"!==g?T(s.y):1,e.signX||(e.signX=h),e.signY||(e.signY=l),u.lockScalingFlip&&(e.signX!==h||e.signY!==l))return!1;if(c=u._getTransformedDimensions(),p&&!g){var b=Math.abs(s.x)+Math.abs(s.y),y=e.original,_=b/(Math.abs(c.x*y.scaleX/u.scaleX)+Math.abs(c.y*y.scaleY/u.scaleY));o=y.scaleX*_,a=y.scaleY*_}else o=Math.abs(s.x*u.scaleX/c.x),a=Math.abs(s.y*u.scaleY/c.y);O(e)&&(o*=2,a*=2),e.signX!==h&&"y"!==g&&(e.originX=S[e.originX],o*=-1,e.signX=h),e.signY!==l&&"x"!==g&&(e.originY=S[e.originY],a*=-1,e.signY=l)}var x=u.scaleX,C=u.scaleY;return g?("x"===g&&u.set("scaleX",o),"y"===g&&u.set("scaleY",a)):(!f&&u.set("scaleX",o),!d&&u.set("scaleY",a)),x!==u.scaleX||C!==u.scaleY}n.scaleCursorStyleHandler=function(t,e,i){var r=w(t,i),n="";if(0!==e.x&&0===e.y?n="x":0===e.x&&0!==e.y&&(n="y"),k(i,n,r))return"not-allowed";var s=a(i,e);return o[s]+"-resize"},n.skewCursorStyleHandler=function(t,e,i){var r="not-allowed";if(0!==e.x&&i.lockSkewingY)return r;if(0!==e.y&&i.lockSkewingX)return r;var n=a(i,e)%4;return s[n]+"-resize"},n.scaleSkewCursorStyleHandler=function(t,e,i){return t[i.canvas.altActionKey]?n.skewCursorStyleHandler(t,e,i):n.scaleCursorStyleHandler(t,e,i)},n.rotationWithSnapping=b("rotating",m(function(t,e,i,r){var n=e,s=n.target,o=s.translateToOriginPoint(s.getCenterPoint(),n.originX,n.originY);if(s.lockRotation)return!1;var a,c=Math.atan2(n.ey-o.y,n.ex-o.x),h=Math.atan2(r-o.y,i-o.x),l=v(h-c+n.theta);if(0o.r2,h=this.gradientTransform?this.gradientTransform.concat():fabric.iMatrix.concat(),l=-this.offsetX,u=-this.offsetY,f=!!e.additionalTransform,d="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(a.sort(function(t,e){return t.offset-e.offset}),"objectBoundingBox"===d?(l/=t.width,u/=t.height):(l+=t.width/2,u+=t.height/2),"path"===t.type&&"percentage"!==this.gradientUnits&&(l-=t.pathOffset.x,u-=t.pathOffset.y),h[4]-=l,h[5]-=u,s='id="SVGID_'+this.id+'" gradientUnits="'+d+'"',s+=' gradientTransform="'+(f?e.additionalTransform+" ":"")+fabric.util.matrixToSVG(h)+'" ',"linear"===this.type?n=["\n']:"radial"===this.type&&(n=["\n']),"radial"===this.type){if(c)for((a=a.concat()).reverse(),i=0,r=a.length;i\n')}return n.push("linear"===this.type?"\n":"\n"),n.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var o=t.fabric||(t.fabric={}),a=o.util.toFixed;o.Shadow?o.warn("fabric.Shadow is already defined."):(o.Shadow=o.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){for(var e in"string"==typeof t&&(t=this._parseShadow(t)),t)this[e]=t[e];this.id=o.Object.__uid++},_parseShadow:function(t){var e=t.trim(),i=o.Shadow.reOffsetsAndBlur.exec(e)||[];return{color:(e.replace(o.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseFloat(i[1],10)||0,offsetY:parseFloat(i[2],10)||0,blur:parseFloat(i[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var e=40,i=40,r=o.Object.NUM_FRACTION_DIGITS,n=o.util.rotateVector({x:this.offsetX,y:this.offsetY},o.util.degreesToRadians(-t.angle)),s=new o.Color(this.color);return t.width&&t.height&&(e=100*a((Math.abs(n.x)+this.blur)/t.width,r)+20,i=100*a((Math.abs(n.y)+this.blur)/t.height,r)+20),t.flipX&&(n.x*=-1),t.flipY&&(n.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke,nonScaling:this.nonScaling};var e={},i=o.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach(function(t){this[t]!==i[t]&&(e[t]=this[t])},this),e}}),o.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/)}("undefined"!=typeof exports?exports:this),function(){"use strict";if(fabric.StaticCanvas)fabric.warn("fabric.StaticCanvas is already defined.");else{var n=fabric.util.object.extend,t=fabric.util.getElementOffset,h=fabric.util.removeFromArray,a=fabric.util.toFixed,s=fabric.util.transformPoint,o=fabric.util.invertTransform,i=fabric.util.getNodeCanvas,r=fabric.util.createCanvasElement,e=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1!==fabric.devicePixelRatio&&this.enableRetinaScaling},getRetinaScaling:function(){return this._isRetinaScaling()?fabric.devicePixelRatio:1},_initRetinaScaling:function(){if(this._isRetinaScaling()){var t=fabric.devicePixelRatio;this.__initRetinaScaling(t,this.lowerCanvasEl,this.contextContainer),this.upperCanvasEl&&this.__initRetinaScaling(t,this.upperCanvasEl,this.contextTop)}},__initRetinaScaling:function(t,e,i){e.setAttribute("width",this.width*t),e.setAttribute("height",this.height*t),i.scale(t,t)},calcOffset:function(){return this._offset=t(this.lowerCanvasEl),this},setOverlayImage:function(t,e,i){return this.__setBgOverlayImage("overlayImage",t,e,i)},setBackgroundImage:function(t,e,i){return this.__setBgOverlayImage("backgroundImage",t,e,i)},setOverlayColor:function(t,e){return this.__setBgOverlayColor("overlayColor",t,e)},setBackgroundColor:function(t,e){return this.__setBgOverlayColor("backgroundColor",t,e)},__setBgOverlayImage:function(r,t,n,s){return"string"==typeof t?fabric.util.loadImage(t,function(t,e){if(t){var i=new fabric.Image(t,s);(this[r]=i).canvas=this}n&&n(t,e)},this,s&&s.crossOrigin):(s&&t.setOptions(s),(this[r]=t)&&(t.canvas=this),n&&n(t,!1)),this},__setBgOverlayColor:function(t,e,i){return this[t]=e,this._initGradient(e,t),this._initPattern(e,t,i),this},_createCanvasElement:function(){var t=r();if(!t)throw e;if(t.style||(t.style={}),void 0===t.getContext)throw e;return t},_initOptions:function(t){var e=this.lowerCanvasEl;this._setOptions(t),this.width=this.width||parseInt(e.width,10)||0,this.height=this.height||parseInt(e.height,10)||0,this.lowerCanvasEl.style&&(e.width=this.width,e.height=this.height,e.style.width=this.width+"px",e.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice())},_createLowerCanvas:function(t){t&&t.getContext?this.lowerCanvasEl=t:this.lowerCanvasEl=fabric.util.getById(t)||this._createCanvasElement(),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this._originalCanvasStyle=this.lowerCanvasEl.style,this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(t,e){return this.setDimensions({width:t},e)},setHeight:function(t,e){return this.setDimensions({height:t},e)},setDimensions:function(t,e){var i;for(var r in e=e||{},t)i=t[r],e.cssOnly||(this._setBackstoreDimension(r,t[r]),i+="px",this.hasLostContext=!0),e.backstoreOnly||this._setCssDimension(r,i);return this._isCurrentlyDrawing&&this.freeDrawingBrush&&this.freeDrawingBrush._setBrushStyles(),this._initRetinaScaling(),this.calcOffset(),e.cssOnly||this.requestRenderAll(),this},_setBackstoreDimension:function(t,e){return this.lowerCanvasEl[t]=e,this.upperCanvasEl&&(this.upperCanvasEl[t]=e),this.cacheCanvasEl&&(this.cacheCanvasEl[t]=e),this[t]=e,this},_setCssDimension:function(t,e){return this.lowerCanvasEl.style[t]=e,this.upperCanvasEl&&(this.upperCanvasEl.style[t]=e),this.wrapperEl&&(this.wrapperEl.style[t]=e),this},getZoom:function(){return this.viewportTransform[0]},setViewportTransform:function(t){var e,i,r,n=this._activeObject,s=this.backgroundImage,o=this.overlayImage;for(this.viewportTransform=t,i=0,r=this._objects.length;i\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("
    \n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,r=e.width||this.width,n=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',o=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+a(-i[4]/i[0],o)+" "+a(-i[5]/i[3],o)+" "+a(this.width/i[0],o)+" "+a(this.height/i[3],o)+'" '),t.push("\n',"Created with Fabric.js ",fabric.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),this.createSVGClipPathMarkup(e),"\n")},createSVGClipPathMarkup:function(t){var e=this.clipPath;return e?(e.clipPathId="CLIPPATH_"+fabric.Object.__uid++,'\n'+this.clipPath.toClipPathSVG(t.reviver)+"\n"):""},createSVGRefElementsMarkup:function(){var s=this;return["background","overlay"].map(function(t){var e=s[t+"Color"];if(e&&e.toLive){var i=s[t+"Vpt"],r=s.viewportTransform,n={width:s.width/(i?r[0]:1),height:s.height/(i?r[3]:1)};return e.toSVG(n,{additionalTransform:i?fabric.util.matrixToSVG(r):""})}}).join("")},createSVGFontFacesMarkup:function(){var t,e,i,r,n,s,o,a,c="",h={},l=fabric.fontPaths,u=[];for(this._objects.forEach(function t(e){u.push(e),e._objects&&e._objects.forEach(t)}),o=0,a=u.length;o',"\n",c,"","\n"].join("")),c},_setSVGObjects:function(t,e){var i,r,n,s=this._objects;for(r=0,n=s.length;r
    \n")}else t.push('\n")},sendToBack:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(e=(r=n._objects).length;e--;)i=r[e],h(this._objects,i),this._objects.unshift(i);else h(this._objects,t),this._objects.unshift(t);return this.renderOnAddRemove&&this.requestRenderAll(),this},bringToFront:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(r=n._objects,e=0;e"}}),n(fabric.StaticCanvas.prototype,fabric.Observable),n(fabric.StaticCanvas.prototype,fabric.Collection),n(fabric.StaticCanvas.prototype,fabric.DataURLExporter),n(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=r();if(!e||!e.getContext)return null;var i=e.getContext("2d");if(!i)return null;switch(t){case"setLineDash":return void 0!==i.setLineDash;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject,fabric.isLikelyNode&&(fabric.StaticCanvas.prototype.createPNGStream=function(){var t=i(this.lowerCanvasEl);return t&&t.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(t){var e=i(this.lowerCanvasEl);return e&&e.createJPEGStream(t)})}}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeMiterLimit:10,strokeDashArray:null,limitedToCanvasSize:!1,_setBrushStyles:function(){var t=this.canvas.contextTop;t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.miterLimit=this.strokeMiterLimit,t.lineJoin=this.strokeLineJoin,t.setLineDash(this.strokeDashArray||[])},_saveAndTransform:function(t){var e=this.canvas.viewportTransform;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5])},_setShadow:function(){if(this.shadow){var t=this.canvas,e=this.shadow,i=t.contextTop,r=t.getZoom();t&&t._isRetinaScaling()&&(r*=fabric.devicePixelRatio),i.shadowColor=e.color,i.shadowBlur=e.blur*r,i.shadowOffsetX=e.offsetX*r,i.shadowOffsetY=e.offsetY*r}},needsFullRender:function(){return new fabric.Color(this.color).getAlpha()<1||!!this.shadow},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0},_isOutSideCanvas:function(t){return t.x<0||t.x>this.canvas.getWidth()||t.y<0||t.y>this.canvas.getHeight()}}),fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{decimate:.4,initialize:function(t){this.canvas=t,this._points=[]},_drawSegment:function(t,e,i){var r=e.midPointFrom(i);return t.quadraticCurveTo(e.x,e.y,r.x,r.y),r},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this._prepareForDrawing(t),this._captureDrawingPath(t),this._render())},onMouseMove:function(t,e){if(this.canvas._isMainEvent(e.e)&&(!0!==this.limitedToCanvasSize||!this._isOutSideCanvas(t))&&this._captureDrawingPath(t)&&1"},getObjectScaling:function(){if(!this.group)return{scaleX:this.scaleX,scaleY:this.scaleY};var t=x.util.qrDecompose(this.calcTransformMatrix());return{scaleX:Math.abs(t.scaleX),scaleY:Math.abs(t.scaleY)}},getTotalObjectScaling:function(){var t=this.getObjectScaling(),e=t.scaleX,i=t.scaleY;if(this.canvas){var r=this.canvas.getZoom(),n=this.canvas.getRetinaScaling();e*=r*n,i*=r*n}return{scaleX:e,scaleY:i}},getObjectOpacity:function(){var t=this.opacity;return this.group&&(t*=this.group.getObjectOpacity()),t},_set:function(t,e){var i="scaleX"===t||"scaleY"===t,r=this[t]!==e,n=!1;return i&&(e=this._constrainScale(e)),"scaleX"===t&&e<0?(this.flipX=!this.flipX,e*=-1):"scaleY"===t&&e<0?(this.flipY=!this.flipY,e*=-1):"shadow"!==t||!e||e instanceof x.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",e):e=new x.Shadow(e),this[t]=e,r&&(n=this.group&&this.group.isOnACache(),-1=t.x&&n.left+n.width<=e.x&&n.top>=t.y&&n.top+n.height<=e.y},containsPoint:function(t,e,i,r){var n=this._getCoords(i,r),s=(e=e||this._getImageLines(n),this._findCrossPoints(t,e));return 0!==s&&s%2==1},isOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.getCoords(!0,t).some(function(t){return t.x<=i.x&&t.x>=e.x&&t.y<=i.y&&t.y>=e.y})||(!!this.intersectsWithRect(e,i,!0,t)||this._containsCenterOfCanvas(e,i,t))},_containsCenterOfCanvas:function(t,e,i){var r={x:(t.x+e.x)/2,y:(t.y+e.y)/2};return!!this.containsPoint(r,null,!0,i)},isPartiallyOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.intersectsWithRect(e,i,!0,t)||this.getCoords(!0,t).every(function(t){return(t.x>=i.x||t.x<=e.x)&&(t.y>=i.y||t.y<=e.y)})&&this._containsCenterOfCanvas(e,i,t)},_getImageLines:function(t){return{topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}}},_findCrossPoints:function(t,e){var i,r,n,s=0;for(var o in e)if(!((n=e[o]).o.y=t.y&&n.d.y>=t.y||(n.o.x===n.d.x&&n.o.x>=t.x?r=n.o.x:(0,i=(n.d.y-n.o.y)/(n.d.x-n.o.x),r=-(t.y-0*t.x-(n.o.y-i*n.o.x))/(0-i)),r>=t.x&&(s+=1),2!==s)))break;return s},getBoundingRect:function(t,e){var i=this.getCoords(t,e);return h.makeBoundingBoxFromPoints(i)},getScaledWidth:function(){return this._getTransformedDimensions().x},getScaledHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)
    \n')}},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(t),{reviver:t})},toClipPathSVG:function(t){return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(t),{reviver:t})},_createBaseClipPathSVGMarkup:function(t,e){var i=(e=e||{}).reviver,r=e.additionalTransform||"",n=[this.getSvgTransform(!0,r),this.getSvgCommons()].join(""),s=t.indexOf("COMMON_PARTS");return t[s]=n,i?i(t.join("")):t.join("")},_createBaseSVGMarkup:function(t,e){var i,r,n=(e=e||{}).noStyle,s=e.reviver,o=n?"":'style="'+this.getSvgStyles()+'" ',a=e.withShadow?'style="'+this.getSvgFilter()+'" ':"",c=this.clipPath,h=this.strokeUniform?'vector-effect="non-scaling-stroke" ':"",l=c&&c.absolutePositioned,u=this.stroke,f=this.fill,d=this.shadow,g=[],p=t.indexOf("COMMON_PARTS"),v=e.additionalTransform;return c&&(c.clipPathId="CLIPPATH_"+fabric.Object.__uid++,r='\n'+c.toClipPathSVG(s)+"\n"),l&&g.push("\n"),g.push("\n"),i=[o,h,n?"":this.addPaintOrder()," ",v?'transform="'+v+'" ':""].join(""),t[p]=i,f&&f.toLive&&g.push(f.toSVG(this)),u&&u.toLive&&g.push(u.toSVG(this)),d&&g.push(d.toSVG(this)),c&&g.push(r),g.push(t.join("")),g.push("\n"),l&&g.push("\n"),s?s(g.join("")):g.join("")},addPaintOrder:function(){return"fill"!==this.paintFirst?' paint-order="'+this.paintFirst+'" ':""}})}(),function(){var n=fabric.util.object.extend,r="stateProperties";function s(e,t,i){var r={};i.forEach(function(t){r[t]=e[t]}),n(e[t],r,!0)}fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(t){var e="_"+(t=t||r);return Object.keys(this[e]).length\n']}}),s.Line.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),s.Line.fromElement=function(t,e,i){i=i||{};var r=s.parseAttributes(t,s.Line.ATTRIBUTE_NAMES),n=[r.x1||0,r.y1||0,r.x2||0,r.y2||0];e(new s.Line(n,o(r,i)))},s.Line.fromObject=function(t,e){var i=r(t,!0);i.points=[t.x1,t.y1,t.x2,t.y2],s.Object._fromObject("Line",i,function(t){delete t.points,e&&e(t)},"points")})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var a=t.fabric||(t.fabric={}),c=Math.PI;a.Circle?a.warn("fabric.Circle is already defined."):(a.Circle=a.util.createClass(a.Object,{type:"circle",radius:0,startAngle:0,endAngle:2*c,cacheProperties:a.Object.prototype.cacheProperties.concat("radius","startAngle","endAngle"),_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},_toSVG:function(){var t,e=(this.endAngle-this.startAngle)%(2*c);if(0===e)t=["\n'];else{var i=a.util.cos(this.startAngle)*this.radius,r=a.util.sin(this.startAngle)*this.radius,n=a.util.cos(this.endAngle)*this.radius,s=a.util.sin(this.endAngle)*this.radius,o=c\n"]}return t},_render:function(t){t.beginPath(),t.arc(0,0,this.radius,this.startAngle,this.endAngle,!1),this._renderPaintInOrder(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),a.Circle.ATTRIBUTE_NAMES=a.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),a.Circle.fromElement=function(t,e){var i,r=a.parseAttributes(t,a.Circle.ATTRIBUTE_NAMES);if(!("radius"in(i=r)&&0<=i.radius))throw new Error("value of `r` attribute is required and can not be negative");r.left=(r.left||0)-r.radius,r.top=(r.top||0)-r.radius,e(new a.Circle(r))},a.Circle.fromObject=function(t,e){a.Object._fromObject("Circle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var i=t.fabric||(t.fabric={});i.Triangle?i.warn("fabric.Triangle is already defined"):(i.Triangle=i.util.createClass(i.Object,{type:"triangle",width:100,height:100,_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderPaintInOrder(t)},_toSVG:function(){var t=this.width/2,e=this.height/2;return["']}}),i.Triangle.fromObject=function(t,e){return i.Object._fromObject("Triangle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var r=t.fabric||(t.fabric={}),e=2*Math.PI;r.Ellipse?r.warn("fabric.Ellipse is already defined."):(r.Ellipse=r.util.createClass(r.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:r.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']},_render:function(t){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(0,0,this.rx,0,e,!1),t.restore(),this._renderPaintInOrder(t)}}),r.Ellipse.ATTRIBUTE_NAMES=r.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),r.Ellipse.fromElement=function(t,e){var i=r.parseAttributes(t,r.Ellipse.ATTRIBUTE_NAMES);i.left=(i.left||0)-i.rx,i.top=(i.top||0)-i.ry,e(new r.Ellipse(i))},r.Ellipse.fromObject=function(t,e){r.Object._fromObject("Ellipse",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var s=t.fabric||(t.fabric={}),o=s.util.object.extend;s.Rect?s.warn("fabric.Rect is already defined"):(s.Rect=s.util.createClass(s.Object,{stateProperties:s.Object.prototype.stateProperties.concat("rx","ry"),type:"rect",rx:0,ry:0,cacheProperties:s.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t){var e=this.rx?Math.min(this.rx,this.width/2):0,i=this.ry?Math.min(this.ry,this.height/2):0,r=this.width,n=this.height,s=-this.width/2,o=-this.height/2,a=0!==e||0!==i,c=.4477152502;t.beginPath(),t.moveTo(s+e,o),t.lineTo(s+r-e,o),a&&t.bezierCurveTo(s+r-c*e,o,s+r,o+c*i,s+r,o+i),t.lineTo(s+r,o+n-i),a&&t.bezierCurveTo(s+r,o+n-c*i,s+r-c*e,o+n,s+r-e,o+n),t.lineTo(s+e,o+n),a&&t.bezierCurveTo(s+c*e,o+n,s,o+n-c*i,s,o+n-i),t.lineTo(s,o+i),a&&t.bezierCurveTo(s,o+c*i,s+c*e,o,s+e,o),t.closePath(),this._renderPaintInOrder(t)},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']}}),s.Rect.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),s.Rect.fromElement=function(t,e,i){if(!t)return e(null);i=i||{};var r=s.parseAttributes(t,s.Rect.ATTRIBUTE_NAMES);r.left=r.left||0,r.top=r.top||0,r.height=r.height||0,r.width=r.width||0;var n=new s.Rect(o(i?s.util.object.clone(i):{},r));n.visible=n.visible&&0\n']},commonRender:function(t){var e,i=this.points.length,r=this.pathOffset.x,n=this.pathOffset.y;if(!i||isNaN(this.points[i-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-r,this.points[0].y-n);for(var s=0;s"},toObject:function(t){return n(this.callSuper("toObject",t),{path:this.path.map(function(t){return t.slice()})})},toDatalessObject:function(t){var e=this.toObject(["sourcePath"].concat(t));return e.sourcePath&&delete e.path,e},_toSVG:function(){return["\n"]},_getOffsetTransform:function(){var t=f.Object.NUM_FRACTION_DIGITS;return" translate("+e(-this.pathOffset.x,t)+", "+e(-this.pathOffset.y,t)+")"},toClipPathSVG:function(t){var e=this._getOffsetTransform();return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},toSVG:function(t){var e=this._getOffsetTransform();return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},complexity:function(){return this.path.length},_calcDimensions:function(){for(var t,e,i=[],r=[],n=0,s=0,o=0,a=0,c=0,h=this.path.length;c"},addWithUpdate:function(t){var e=!!this.group;return this._restoreObjectsState(),h.util.resetObjectTransform(this),t&&(e&&h.util.removeTransformFromObject(t,this.group.calcTransformMatrix()),this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,e?this.group.addWithUpdate():this.setCoords(),this},removeWithUpdate:function(t){return this._restoreObjectsState(),h.util.resetObjectTransform(this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.setCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group},_set:function(t,e){var i=this._objects.length;if(this.useSetOnGroup)for(;i--;)this._objects[i].setOnGroup(t,e);if("canvas"===t)for(;i--;)this._objects[i]._set(t,e);h.Object.prototype._set.call(this,t,e)},toObject:function(r){var n=this.includeDefaultValues,t=this._objects.filter(function(t){return!t.excludeFromExport}).map(function(t){var e=t.includeDefaultValues;t.includeDefaultValues=n;var i=t.toObject(r);return t.includeDefaultValues=e,i}),e=h.Object.prototype.toObject.call(this,r);return e.objects=t,e},toDatalessObject:function(r){var t,e=this.sourcePath;if(e)t=e;else{var n=this.includeDefaultValues;t=this._objects.map(function(t){var e=t.includeDefaultValues;t.includeDefaultValues=n;var i=t.toDatalessObject(r);return t.includeDefaultValues=e,i})}var i=h.Object.prototype.toDatalessObject.call(this,r);return i.objects=t,i},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=h.Object.prototype.shouldCache.call(this);if(t)for(var e=0,i=this._objects.length;e\n"],i=0,r=this._objects.length;i\n"),e},getSvgStyles:function(){var t=void 0!==this.opacity&&1!==this.opacity?"opacity: "+this.opacity+";":"",e=this.visible?"":" visibility: hidden;";return[t,this.getSvgFilter(),e].join("")},toClipPathSVG:function(t){for(var e=[],i=0,r=this._objects.length;i"},shouldCache:function(){return!1},isOnACache:function(){return!1},_renderControls:function(t,e,i){t.save(),t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,this.callSuper("_renderControls",t,e),void 0===(i=i||{}).hasControls&&(i.hasControls=!1),i.forActiveSelection=!0;for(var r=0,n=this._objects.length;r\n','\t\n',"\n"),o=' clip-path="url(#imageCrop_'+c+')" '}if(this.imageSmoothing||(a='" image-rendering="optimizeSpeed'),i.push("\t\n"),this.stroke||this.strokeDashArray){var h=this.fill;this.fill=null,t=["\t\n'],this.fill=h}return e="fill"!==this.paintFirst?e.concat(t,i):e.concat(i,t)},getSrc:function(t){var e=t?this._element:this._originalElement;return e?e.toDataURL?e.toDataURL():this.srcFromAttribute?e.getAttribute("src"):e.src:this.src||""},setSrc:function(t,i,r){return fabric.util.loadImage(t,function(t,e){this.setElement(t,r),this._setWidthHeight(),i&&i(this,e)},this,r&&r.crossOrigin),this},toString:function(){return'#'},applyResizeFilters:function(){var t=this.resizeFilter,e=this.minimumScaleTrigger,i=this.getTotalObjectScaling(),r=i.scaleX,n=i.scaleY,s=this._filteredEl||this._originalElement;if(this.group&&this.set("dirty",!0),!t||e=t;for(var a=["highp","mediump","lowp"],c=0;c<3;c++)if(void 0,i="precision "+a[c]+" float;\nvoid main(){}",r=(e=s).createShader(e.FRAGMENT_SHADER),e.shaderSource(r,i),e.compileShader(r),e.getShaderParameter(r,e.COMPILE_STATUS)){fabric.webGlPrecision=a[c];break}}return this.isSupported=o},(fabric.WebglFilterBackend=t).prototype={tileSize:2048,resources:{},setupGLContext:function(t,e){this.dispose(),this.createWebGLCanvas(t,e),this.aPosition=new Float32Array([0,0,0,1,1,0,1,1]),this.chooseFastestCopyGLTo2DMethod(t,e)},chooseFastestCopyGLTo2DMethod:function(t,e){var i,r=void 0!==window.performance;try{new ImageData(1,1),i=!0}catch(t){i=!1}var n="undefined"!=typeof ArrayBuffer,s="undefined"!=typeof Uint8ClampedArray;if(r&&i&&n&&s){var o=fabric.util.createCanvasElement(),a=new ArrayBuffer(t*e*4);if(fabric.forceGLPutImageData)return this.imageBuffer=a,void(this.copyGLTo2D=copyGLTo2DPutImageData);var c,h,l={imageBuffer:a,destinationWidth:t,destinationHeight:e,targetCanvas:o};o.width=t,o.height=e,c=window.performance.now(),copyGLTo2DDrawImage.call(l,this.gl,l),h=window.performance.now()-c,c=window.performance.now(),copyGLTo2DPutImageData.call(l,this.gl,l),window.performance.now()-c 0.0) {\n"+this.fragmentSource[t]+"}\n}"},retrieveShader:function(t){var e,i=this.type+"_"+this.mode;return t.programCache.hasOwnProperty(i)||(e=this.buildSource(this.mode),t.programCache[i]=this.createProgram(t.context,e)),t.programCache[i]},applyTo2d:function(t){var e,i,r,n,s,o,a,c=t.imageData.data,h=c.length,l=1-this.alpha;e=(a=new f.Color(this.color).getSource())[0]*this.alpha,i=a[1]*this.alpha,r=a[2]*this.alpha;for(var u=0;u'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){var e=this.path;e&&!e.isNotVisible()&&e._render(t),this._setTextStyles(t),this._renderTextLinesBackground(t),this._renderTextDecoration(t,"underline"),this._renderText(t),this._renderTextDecoration(t,"overline"),this._renderTextDecoration(t,"linethrough")},_renderText:function(t){"stroke"===this.paintFirst?(this._renderTextStroke(t),this._renderTextFill(t)):(this._renderTextFill(t),this._renderTextStroke(t))},_setTextStyles:function(t,e,i){t.textBaseline="alphabetic",t.font=this._getFontDeclaration(e,i)},calcTextWidth:function(){for(var t=this.getLineWidth(0),e=1,i=this._textLines.length;ethis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=e):(this.selectionStart=e,this.selectionEnd=this.__selectionStartOnMouseDown),this.selectionStart===i&&this.selectionEnd===r||(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection()))}},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},fromStringToGraphemeSelection:function(t,e,i){var r=i.slice(0,t),n=fabric.util.string.graphemeSplit(r).length;if(t===e)return{selectionStart:n,selectionEnd:n};var s=i.slice(t,e);return{selectionStart:n,selectionEnd:n+fabric.util.string.graphemeSplit(s).length}},fromGraphemeToStringSelection:function(t,e,i){var r=i.slice(0,t).join("").length;return t===e?{selectionStart:r,selectionEnd:r}:{selectionStart:r,selectionEnd:r+i.slice(t,e).join("").length}},_updateTextarea:function(){if(this.cursorOffsetCache={},this.hiddenTextarea){if(!this.inCompositionMode){var t=this.fromGraphemeToStringSelection(this.selectionStart,this.selectionEnd,this._text);this.hiddenTextarea.selectionStart=t.selectionStart,this.hiddenTextarea.selectionEnd=t.selectionEnd}this.updateTextareaPosition()}},updateFromTextArea:function(){if(this.hiddenTextarea){this.cursorOffsetCache={},this.text=this.hiddenTextarea.value,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords());var t=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value);this.selectionEnd=this.selectionStart=t.selectionEnd,this.inCompositionMode||(this.selectionStart=t.selectionStart),this.updateTextareaPosition()}},updateTextareaPosition:function(){if(this.selectionStart===this.selectionEnd){var t=this._calcTextareaPosition();this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top}},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.inCompositionMode?this.compositionStart:this.selectionStart,e=this._getCursorBoundaries(t),i=this.get2DCursorLocation(t),r=i.lineIndex,n=i.charIndex,s=this.getValueOfPropertyAt(r,n,"fontSize")*this.lineHeight,o=e.leftOffset,a=this.calcTransformMatrix(),c={x:e.left+o,y:e.top+e.topOffset+s},h=this.canvas.getRetinaScaling(),l=this.canvas.upperCanvasEl,u=l.width/h,f=l.height/h,d=u-s,g=f-s,p=l.clientWidth/u,v=l.clientHeight/f;return c=fabric.util.transformPoint(c,a),(c=fabric.util.transformPoint(c,this.canvas.viewportTransform)).x*=p,c.y*=v,c.x<0&&(c.x=0),c.x>d&&(c.x=d),c.y<0&&(c.y=0),c.y>g&&(c.y=g),c.x+=this.canvas._offset.left,c.y+=this.canvas._offset.top,{left:c.x+"px",top:c.y+"px",fontSize:s+"px",charHeight:s}},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,selectable:this.selectable,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){this._savedProps&&(this.hoverCursor=this._savedProps.hoverCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.selectable=this._savedProps.selectable,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text,e=this.hiddenTextarea;return this.selected=!1,this.isEditing=!1,this.selectionEnd=this.selectionStart,e&&(e.blur&&e.blur(),e.parentNode&&e.parentNode.removeChild(e)),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},removeStyleFromTo:function(t,e){var i,r,n=this.get2DCursorLocation(t,!0),s=this.get2DCursorLocation(e,!0),o=n.lineIndex,a=n.charIndex,c=s.lineIndex,h=s.charIndex;if(o!==c){if(this.styles[o])for(i=a;it?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown)},onMouseDown:function(t){if(this.canvas){this.__newClickTime=+new Date;var e=t.pointer;this.isTripleClick(e)&&(this.fire("tripleclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected}},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},doubleClickHandler:function(t){this.isEditing&&this.selectWord(this.getSelectionStartFromPointer(t.e))},tripleClickHandler:function(t){this.isEditing&&this.selectLine(this.getSelectionStartFromPointer(t.e))},initClicks:function(){this.on("mousedblclick",this.doubleClickHandler),this.on("tripleclick",this.tripleClickHandler)},_mouseDownHandler:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.__isMousedown=!0,this.selected&&(this.inCompositionMode=!1,this.setCursorByClick(t.e)),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection()))},_mouseDownHandlerBefore:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.selected=this===this.canvas._activeObject)},initMousedownHandler:function(){this.on("mousedown",this._mouseDownHandler),this.on("mousedown:before",this._mouseDownHandlerBefore)},initMouseupHandler:function(){this.on("mouseup",this.mouseUpHandler)},mouseUpHandler:function(t){if(this.__isMousedown=!1,!(!this.editable||this.group||t.transform&&t.transform.actionPerformed||t.e.button&&1!==t.e.button)){if(this.canvas){var e=this.canvas._activeObject;if(e&&e!==this)return}this.__lastSelected&&!this.__corner?(this.selected=!1,this.__lastSelected=!1,this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()):this.selected=!0}},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,r=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,r,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e,i=this.getLocalPointer(t),r=0,n=0,s=0,o=0,a=0,c=0,h=this._textLines.length;cthis._text.length&&(a=this._text.length),a}}),fabric.util.object.extend(fabric.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.setAttribute("autocorrect","off"),this.hiddenTextarea.setAttribute("autocomplete","off"),this.hiddenTextarea.setAttribute("spellcheck","false"),this.hiddenTextarea.setAttribute("data-fabric-hiddentextarea",""),this.hiddenTextarea.setAttribute("wrap","off");var t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="position: absolute; top: "+t.top+"; left: "+t.left+"; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; paddingーtop: "+t.fontSize+";",this.hiddenTextareaContainer?this.hiddenTextareaContainer.appendChild(this.hiddenTextarea):fabric.document.body.appendChild(this.hiddenTextarea),fabric.util.addListener(this.hiddenTextarea,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(this.hiddenTextarea,"keyup",this.onKeyUp.bind(this)),fabric.util.addListener(this.hiddenTextarea,"input",this.onInput.bind(this)),fabric.util.addListener(this.hiddenTextarea,"copy",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"cut",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"paste",this.paste.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionstart",this.onCompositionStart.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionupdate",this.onCompositionUpdate.bind(this)),fabric.util.addListener(this.hiddenTextarea,"compositionend",this.onCompositionEnd.bind(this)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},keysMap:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown"},keysMapRtl:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorLeft",36:"moveCursorRight",37:"moveCursorRight",38:"moveCursorUp",39:"moveCursorLeft",40:"moveCursorDown"},ctrlKeysMapUp:{67:"copy",88:"cut"},ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){var e="rtl"===this.direction?this.keysMapRtl:this.keysMap;if(t.keyCode in e)this[e[t.keyCode]](t);else{if(!(t.keyCode in this.ctrlKeysMapDown&&(t.ctrlKey||t.metaKey)))return;this[this.ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),33<=t.keyCode&&t.keyCode<=40?(this.inCompositionMode=!1,this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.requestRenderAll()}},onKeyUp:function(t){!this.isEditing||this._copyDone||this.inCompositionMode?this._copyDone=!1:t.keyCode in this.ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this.ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.requestRenderAll())},onInput:function(t){var e=this.fromPaste;if(this.fromPaste=!1,t&&t.stopPropagation(),this.isEditing){var i,r,n,s,o,a=this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,c=this._text.length,h=a.length,l=h-c,u=this.selectionStart,f=this.selectionEnd,d=u!==f;if(""===this.hiddenTextarea.value)return this.styles={},this.updateFromTextArea(),this.fire("changed"),void(this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll()));var g=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),p=u>g.selectionStart;d?(i=this._text.slice(u,f),l+=f-u):h=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){var i=this["get"+t+"CursorOffset"](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(i):this.moveCursorWithoutShift(i),0!==i&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return t<0?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var r;if(t.altKey)r=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;r=this["findLineBoundary"+i](this[e])}if(void 0!==typeof r&&this[e]!==r)return this[e]=r,!0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){var i="moveCursor"+t+"With";this._currentCursorOpacity=1,e.shiftKey?i+="Shift":i+="outShift",this[i](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this._text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t,e){void 0===e&&(e=t+1),this.removeStyleFromTo(t,e),this._text.splice(t,e-t),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()},insertChars:function(t,e,i,r){void 0===r&&(r=i),i",t.textSpans.join(""),"\n"]},_getSVGTextAndBg:function(t,e){var i,r=[],n=[],s=t;this._setSVGBg(n);for(var o=0,a=this._textLines.length;o",fabric.util.string.escapeXml(t),""].join("")},_setSVGTextLineText:function(t,e,i,r){var n,s,o,a,c,h=this.getHeightOfLine(e),l=-1!==this.textAlign.indexOf("justify"),u="",f=0,d=this._textLines[e];r+=h*(1-this._fontSizeFraction)/this.lineHeight;for(var g=0,p=d.length-1;g<=p;g++)c=g===p||this.charSpacing,u+=d[g],o=this.__charBounds[e][g],0===f?(i+=o.kernedWidth-o.width,f+=o.width):f+=o.kernedWidth,l&&!c&&this._reSpaceAndTab.test(d[g])&&(c=!0),c||(n=n||this.getCompleteStyleDeclaration(e,g),s=this.getCompleteStyleDeclaration(e,g+1),c=this._hasStyleChangedForSvg(n,s)),c&&(a=this._getStyleDeclaration(e,g)||{},t.push(this._createTextCharSpan(u,a,i,r)),u="",n=s,i+=f,f=0)},_pushTextBgRect:function(t,e,i,r,n,s){var o=fabric.Object.NUM_FRACTION_DIGITS;t.push("\t\t\n')},_setSVGTextLineBg:function(t,e,i,r){for(var n,s,o=this._textLines[e],a=this.getHeightOfLine(e)/this.lineHeight,c=0,h=0,l=this.getValueOfPropertyAt(e,0,"textBackgroundColor"),u=0,f=o.length;uthis.width&&this._set("width",this.dynamicMinWidth),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.height=this.calcTextHeight(),this.saveState({propertySet:"_dimensionAffectingProps"}))},_generateStyleMap:function(t){for(var e=0,i=0,r=0,n={},s=0;sthis.dynamicMinWidth&&(this.dynamicMinWidth=g-v+r),o},isEndOfWrapping:function(t){return!this._styleMap[t+1]||this._styleMap[t+1].line!==this._styleMap[t].line},missingNewlineOffset:function(t){return this.splitByGrapheme?this.isEndOfWrapping(t)?1:0:1},_splitTextIntoLines:function(t){for(var e=b.Text.prototype._splitTextIntoLines.call(this,t),i=this._wrapText(e.lines,this.width),r=new Array(i.length),n=0;nnul + if not errorlevel 1 ( + cmd /k "%~f0" __run__ %* + exit /b + ) +) +shift + +set "ROOT=%~dp0.." +cd /d "%ROOT%" || goto :fail_cd + +set "COMPOSE=docker\docker-compose.yml" +set "ENV_FILE=docker\.env" +set "COMPOSE_PROJECT=soondesign" +set "ACTION=%~1" +set "WEB_PORT=8100" +set "ELECTRON_PORT=8101" +set "BACKEND_PORT=8102" + +if /i "%ACTION%"=="stop" goto :do_stop +if /i "%ACTION%"=="reset" goto :do_reset +goto :do_start + +:compose +docker compose -p "%COMPOSE_PROJECT%" -f "%COMPOSE%" --env-file "%ENV_FILE%" %* +exit /b !ERRORLEVEL! + +:compose_noenv +docker compose -p "%COMPOSE_PROJECT%" -f "%COMPOSE%" %* +exit /b !ERRORLEVEL! + +:do_stop +if exist "%ENV_FILE%" (call :compose down) else (call :compose_noenv down) +echo. +echo [OK] Stopped project %COMPOSE_PROJECT%. DB volume kept. +exit /b 0 + +:do_reset +if exist "%ENV_FILE%" (call :compose down -v) else (call :compose_noenv down -v) +echo. +echo [OK] Project %COMPOSE_PROJECT% stopped. DB volume removed. +exit /b 0 + +:do_start +echo [SoonDesign] Starting local Docker environment... +echo [INFO] Compose project: %COMPOSE_PROJECT% +echo. + +docker info >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Docker is not running. Please start Docker Desktop first. + goto :fail +) + +if not exist "soonModels\manifest.json" ( + echo [ERROR] Missing soonModels\manifest.json + echo Project root: %CD% + goto :fail +) + +if not exist "docker\docker-compose.yml" ( + echo [ERROR] Missing docker\docker-compose.yml + goto :fail +) + +if not exist "%ENV_FILE%" ( + if not exist "docker\.env.example" ( + echo [ERROR] Missing docker\.env.example + goto :fail + ) + copy /Y "docker\.env.example" "%ENV_FILE%" >nul +) + +powershell -NoProfile -ExecutionPolicy Bypass -File "docker\sync-config.ps1" -EnvFile "%ENV_FILE%" +if errorlevel 1 ( + echo [ERROR] Config sync failed. + goto :fail +) + +for /f "usebackq eol=# tokens=1,* delims==" %%A in ("%ENV_FILE%") do ( + if /i "%%A"=="WEB_PORT" set "WEB_PORT=%%B" + if /i "%%A"=="ELECTRON_PORT" set "ELECTRON_PORT=%%B" + if /i "%%A"=="BACKEND_PORT" set "BACKEND_PORT=%%B" +) + +echo [INFO] Removing legacy compose project "docker" if present... +docker compose -p docker -f "%COMPOSE%" --env-file "%ENV_FILE%" down >nul 2>&1 +docker network rm docker_default >nul 2>&1 +call :compose down >nul 2>&1 + +echo [INFO] Ports: Web=%WEB_PORT% Electron=%ELECTRON_PORT% API=%BACKEND_PORT% +echo [INFO] Starting containers - project %COMPOSE_PROJECT%... +call :compose up -d +set "DC_EXIT=!ERRORLEVEL!" +if not "!DC_EXIT!"=="0" ( + echo. + echo [ERROR] docker compose failed with exit code !DC_EXIT!. + echo Ensure Docker Desktop is running. + echo First API start may install pdo_mysql for about 1 minute. + goto :fail +) + +echo [INFO] Waiting for API - first run may take up to 2 min for pdo_mysql install... +set /a RETRY=0 +:wait_health +set /a RETRY+=1 +curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1 +if not errorlevel 1 goto :ready +if !RETRY! GEQ 60 goto :show_urls +ping -n 3 127.0.0.1 >nul +goto :wait_health + +:ready +echo [OK] Backend is ready. + +:show_urls +echo. +echo ======================================== +echo Project: %COMPOSE_PROJECT% +echo Web: http://localhost:%WEB_PORT%/pages/index.web.html +echo Admin: http://localhost:%WEB_PORT%/pages/admin/index.html +echo Electron: http://localhost:%ELECTRON_PORT%/pages/index.html +echo API: http://localhost:%BACKEND_PORT%/health +echo ======================================== +echo Login: admin@local.test / admin123 +echo Stop: scripts\start.bat stop +echo. + +curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1 +if errorlevel 1 ( + echo [WARN] API not ready yet. Check: docker logs soondesign-api + goto :done_ok +) + +curl.exe -sf "http://localhost:%BACKEND_PORT%/api/v1/settings" >nul 2>&1 +if errorlevel 1 ( + echo [WARN] API routes not responding. Check: docker logs soondesign-api +) else ( + echo [OK] API routes verified. +) + +start "" "http://localhost:%WEB_PORT%/pages/index.web.html" +start "" "http://localhost:%WEB_PORT%/pages/admin/index.html" +call :launch_electron +goto :done_ok + +:launch_electron +where node >nul 2>&1 +if errorlevel 1 ( + echo [WARN] Node.js not found. Open Electron preview in browser instead. + start "" "http://localhost:%ELECTRON_PORT%/pages/index.html" + exit /b 0 +) +if not exist "frontend-electron\package.json" ( + echo [WARN] frontend-electron not found. Opening browser preview. + start "" "http://localhost:%ELECTRON_PORT%/pages/index.html" + exit /b 0 +) +if not exist "frontend-electron\node_modules\electron" ( + echo [INFO] Installing Electron dependencies - first run only... + pushd frontend-electron + call npm install --no-audit --no-fund + if errorlevel 1 ( + popd + echo [WARN] npm install failed. Opening browser preview. + start "" "http://localhost:%ELECTRON_PORT%/pages/index.html" + exit /b 0 + ) + popd +) +echo [INFO] Launching Electron desktop app... +start "SoonDesign Desktop" /D "%CD%\frontend-electron" cmd /c "npm start" +exit /b 0 + +:fail_cd +echo [ERROR] Cannot cd to project root: %ROOT% +goto :fail + +:fail +echo. +echo [FAILED] See messages above. +exit /b 1 + +:done_ok +echo. +echo [DONE] SoonDesign is running under Docker project %COMPOSE_PROJECT%. +exit /b 0 diff --git a/soonModels/manifest.json b/soonModels/manifest.json new file mode 100644 index 0000000..bedaf57 --- /dev/null +++ b/soonModels/manifest.json @@ -0,0 +1,4 @@ +{ + "items": [], + "count": 0 +} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 703abc1..0000000 --- a/yarn.lock +++ /dev/null @@ -1,3084 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"7zip-bin@~5.1.1": - version "5.1.1" - resolved "https://registry.npmmirror.com/7zip-bin/-/7zip-bin-5.1.1.tgz#9274ec7460652f9c632c59addf24efb1684ef876" - integrity sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ== - -"@develar/schema-utils@~2.6.5": - version "2.6.5" - resolved "https://registry.npmmirror.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" - integrity sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - -"@discoveryjs/json-ext@^0.5.0": - version "0.5.7" - resolved "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@electron/asar@^3.2.1": - version "3.4.1" - resolved "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065" - integrity sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA== - dependencies: - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - -"@electron/get@^1.13.0", "@electron/get@^1.6.0": - version "1.14.1" - resolved "https://registry.npmmirror.com/@electron/get/-/get-1.14.1.tgz#16ba75f02dffb74c23965e72d617adc721d27f40" - integrity sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw== - dependencies: - debug "^4.1.1" - env-paths "^2.2.0" - fs-extra "^8.1.0" - got "^9.6.0" - progress "^2.0.3" - semver "^6.2.0" - sumchecker "^3.0.1" - optionalDependencies: - global-agent "^3.0.0" - global-tunnel-ng "^2.7.1" - -"@electron/remote@^2.0.8": - version "2.1.3" - resolved "https://registry.npmmirror.com/@electron/remote/-/remote-2.1.3.tgz#fe3341ea6a23c72f663ed611cf7dc2b7784d36d1" - integrity sha512-XlpxC8S4ttj/v2d+PKp9na/3Ev8bV7YWNL7Cw5b9MAWgTphEml7iYgbc7V0r9D6yDOfOkj06bchZgOZdlWJGNA== - -"@electron/universal@1.2.1": - version "1.2.1" - resolved "https://registry.npmmirror.com/@electron/universal/-/universal-1.2.1.tgz#3c2c4ff37063a4e9ab1e6ff57db0bc619bc82339" - integrity sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ== - dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - asar "^3.1.0" - debug "^4.3.1" - dir-compare "^2.4.0" - fs-extra "^9.0.1" - minimatch "^3.0.4" - plist "^3.0.4" - -"@electron/universal@^1.2.1": - version "1.5.1" - resolved "https://registry.npmmirror.com/@electron/universal/-/universal-1.5.1.tgz#f338bc5bcefef88573cf0ab1d5920fac10d06ee5" - integrity sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw== - dependencies: - "@electron/asar" "^3.2.1" - "@malept/cross-spawn-promise" "^1.1.0" - debug "^4.3.1" - dir-compare "^3.0.0" - fs-extra "^9.0.1" - minimatch "^3.0.4" - plist "^3.0.4" - -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== - dependencies: - "@isaacs/balanced-match" "^4.0.1" - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.13" - resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" - integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/source-map@^0.3.3": - version "0.3.11" - resolved "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" - integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": - version "1.5.5" - resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" - integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== - -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.31" - resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" - integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.1" - resolved "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" - integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== - dependencies: - cross-spawn "^7.0.1" - -"@malept/flatpak-bundler@^0.4.0": - version "0.4.0" - resolved "https://registry.npmmirror.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz#e8a32c30a95d20c2b1bb635cc580981a06389858" - integrity sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q== - dependencies: - debug "^4.1.1" - fs-extra "^9.0.0" - lodash "^4.17.15" - tmp-promise "^3.0.2" - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.npmmirror.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.npmmirror.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@types/debug@^4.1.6": - version "4.1.12" - resolved "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" - integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== - dependencies: - "@types/ms" "*" - -"@types/eslint-scope@^3.7.7": - version "3.7.7" - resolved "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "9.6.1" - resolved "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" - integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.8": - version "1.0.8" - resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/fs-extra@^9.0.11": - version "9.0.13" - resolved "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" - integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== - dependencies: - "@types/node" "*" - -"@types/glob@^7.1.1": - version "7.2.0" - resolved "https://registry.npmmirror.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" - integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/minimatch@*": - version "6.0.0" - resolved "https://registry.npmmirror.com/@types/minimatch/-/minimatch-6.0.0.tgz#4d207b1cc941367bdcd195a3a781a7e4fc3b1e03" - integrity sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA== - dependencies: - minimatch "*" - -"@types/ms@*": - version "2.1.0" - resolved "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" - integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== - -"@types/node@*": - version "25.0.3" - resolved "https://registry.npmmirror.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269" - integrity sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA== - dependencies: - undici-types "~7.16.0" - -"@types/node@^14.6.2": - version "14.18.63" - resolved "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" - integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== - -"@types/plist@^3.0.1": - version "3.0.5" - resolved "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" - integrity sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA== - dependencies: - "@types/node" "*" - xmlbuilder ">=11.0.1" - -"@types/verror@^1.10.3": - version "1.10.11" - resolved "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz#d3d6b418978c8aa202d41e5bb3483227b6ecc1bb" - integrity sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg== - -"@types/yargs-parser@*": - version "21.0.3" - resolved "https://registry.npmmirror.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" - integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== - -"@types/yargs@^17.0.1": - version "17.0.35" - resolved "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" - integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== - dependencies: - "@types/yargs-parser" "*" - -"@types/yauzl@^2.9.1": - version "2.10.3" - resolved "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" - integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== - dependencies: - "@types/node" "*" - -"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" - integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== - dependencies: - "@webassemblyjs/helper-numbers" "1.13.2" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - -"@webassemblyjs/floating-point-hex-parser@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" - integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== - -"@webassemblyjs/helper-api-error@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" - integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== - -"@webassemblyjs/helper-buffer@1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" - integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== - -"@webassemblyjs/helper-numbers@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" - integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.13.2" - "@webassemblyjs/helper-api-error" "1.13.2" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" - integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== - -"@webassemblyjs/helper-wasm-section@1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" - integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/wasm-gen" "1.14.1" - -"@webassemblyjs/ieee754@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" - integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" - integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.13.2": - version "1.13.2" - resolved "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" - integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== - -"@webassemblyjs/wasm-edit@^1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" - integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/helper-wasm-section" "1.14.1" - "@webassemblyjs/wasm-gen" "1.14.1" - "@webassemblyjs/wasm-opt" "1.14.1" - "@webassemblyjs/wasm-parser" "1.14.1" - "@webassemblyjs/wast-printer" "1.14.1" - -"@webassemblyjs/wasm-gen@1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" - integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/ieee754" "1.13.2" - "@webassemblyjs/leb128" "1.13.2" - "@webassemblyjs/utf8" "1.13.2" - -"@webassemblyjs/wasm-opt@1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" - integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-buffer" "1.14.1" - "@webassemblyjs/wasm-gen" "1.14.1" - "@webassemblyjs/wasm-parser" "1.14.1" - -"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" - integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@webassemblyjs/helper-api-error" "1.13.2" - "@webassemblyjs/helper-wasm-bytecode" "1.13.2" - "@webassemblyjs/ieee754" "1.13.2" - "@webassemblyjs/leb128" "1.13.2" - "@webassemblyjs/utf8" "1.13.2" - -"@webassemblyjs/wast-printer@1.14.1": - version "1.14.1" - resolved "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" - integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== - dependencies: - "@webassemblyjs/ast" "1.14.1" - "@xtuc/long" "4.2.2" - -"@webpack-cli/configtest@^2.1.1": - version "2.1.1" - resolved "https://registry.npmmirror.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" - integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== - -"@webpack-cli/info@^2.0.2": - version "2.0.2" - resolved "https://registry.npmmirror.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" - integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== - -"@webpack-cli/serve@^2.0.5": - version "2.0.5" - resolved "https://registry.npmmirror.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" - integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== - -"@xmldom/xmldom@^0.8.8": - version "0.8.11" - resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz#b79de2d67389734c57c52595f7a7305e30c2d608" - integrity sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw== - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -acorn-import-phases@^1.0.3: - version "1.0.4" - resolved "https://registry.npmmirror.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" - integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== - -acorn@^8.15.0: - version "8.15.0" - resolved "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" - integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== - -agent-base@6: - version "6.0.2" - resolved "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.4.1: - version "3.5.2" - resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.1.0: - version "5.1.0" - resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@^6.10.0, ajv@^6.12.0: - version "6.12.6" - resolved "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.9.0: - version "8.17.1" - resolved "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -app-builder-bin@4.0.0: - version "4.0.0" - resolved "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz#1df8e654bd1395e4a319d82545c98667d7eed2f0" - integrity sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA== - -app-builder-bin@^4.2.0: - version "4.2.0" - resolved "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-4.2.0.tgz#8ba57c6bbb74510c3ee2c6dc6ad8e2ad7f4074a2" - integrity sha512-PGXlkukQnroTgAaDZnnppdLzsRJmab6Rh/rJ5fKyYaYhd+FfaORH59/ArkB5dr2cAeYQU5lCeHFEwURaoBO8BA== - -app-builder-lib@23.6.0: - version "23.6.0" - resolved "https://registry.npmmirror.com/app-builder-lib/-/app-builder-lib-23.6.0.tgz#03cade02838c077db99d86212d61c5fc1d6da1a8" - integrity sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA== - dependencies: - "7zip-bin" "~5.1.1" - "@develar/schema-utils" "~2.6.5" - "@electron/universal" "1.2.1" - "@malept/flatpak-bundler" "^0.4.0" - async-exit-hook "^2.0.1" - bluebird-lst "^1.0.9" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - chromium-pickle-js "^0.2.0" - debug "^4.3.4" - ejs "^3.1.7" - electron-osx-sign "^0.6.0" - electron-publish "23.6.0" - form-data "^4.0.0" - fs-extra "^10.1.0" - hosted-git-info "^4.1.0" - is-ci "^3.0.0" - isbinaryfile "^4.0.10" - js-yaml "^4.1.0" - lazy-val "^1.0.5" - minimatch "^3.1.2" - read-config-file "6.2.0" - sanitize-filename "^1.6.3" - semver "^7.3.7" - tar "^6.1.11" - temp-file "^3.4.0" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -asar@^3.1.0: - version "3.2.0" - resolved "https://registry.npmmirror.com/asar/-/asar-3.2.0.tgz#e6edb5edd6f627ebef04db62f771c61bea9c1221" - integrity sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg== - dependencies: - chromium-pickle-js "^0.2.0" - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - optionalDependencies: - "@types/glob" "^7.1.1" - -assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async-exit-hook@^2.0.1: - version "2.0.1" - resolved "https://registry.npmmirror.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" - integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== - -async@^3.2.2, async@^3.2.6: - version "3.2.6" - resolved "https://registry.npmmirror.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -author-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/author-regex/-/author-regex-1.0.0.tgz#d08885be6b9bbf9439fe087c76287245f0a81450" - integrity sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1, base64-js@^1.5.1: - version "1.5.1" - resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -baseline-browser-mapping@^2.9.0: - version "2.9.11" - resolved "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz#53724708c8db5f97206517ecfe362dbe5181deea" - integrity sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ== - -bluebird-lst@^1.0.9: - version "1.0.9" - resolved "https://registry.npmmirror.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c" - integrity sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw== - dependencies: - bluebird "^3.5.5" - -bluebird@^3.1.1, bluebird@^3.5.0, bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -boolean@^3.0.1: - version "3.2.0" - resolved "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" - integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== - -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.2" - resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" - integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== - dependencies: - balanced-match "^1.0.0" - -browserslist@^4.28.1: - version "4.28.1" - resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" - integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== - dependencies: - baseline-browser-mapping "^2.9.0" - caniuse-lite "^1.0.30001759" - electron-to-chromium "^1.5.263" - node-releases "^2.0.27" - update-browserslist-db "^1.2.0" - -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.npmmirror.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.npmmirror.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - -buffer-equal@1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ== - -buffer-equal@^1.0.0: - version "1.0.1" - resolved "https://registry.npmmirror.com/buffer-equal/-/buffer-equal-1.0.1.tgz#2f7651be5b1b3f057fcd6e7ee16cf34767077d90" - integrity sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg== - -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.1.0: - version "5.7.1" - resolved "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -builder-util-runtime@9.1.1: - version "9.1.1" - resolved "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz#2da7b34e78a64ad14ccd070d6eed4662d893bd60" - integrity sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw== - dependencies: - debug "^4.3.4" - sax "^1.2.4" - -builder-util@23.6.0: - version "23.6.0" - resolved "https://registry.npmmirror.com/builder-util/-/builder-util-23.6.0.tgz#1880ec6da7da3fd6fa19b8bd71df7f39e8d17dd9" - integrity sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ== - dependencies: - "7zip-bin" "~5.1.1" - "@types/debug" "^4.1.6" - "@types/fs-extra" "^9.0.11" - app-builder-bin "4.0.0" - bluebird-lst "^1.0.9" - builder-util-runtime "9.1.1" - chalk "^4.1.1" - cross-spawn "^7.0.3" - debug "^4.3.4" - fs-extra "^10.0.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-ci "^3.0.0" - js-yaml "^4.1.0" - source-map-support "^0.5.19" - stat-mode "^1.0.0" - temp-file "^3.4.0" - -bytenode@^1.5.7: - version "1.5.7" - resolved "https://registry.npmmirror.com/bytenode/-/bytenode-1.5.7.tgz#94ff87a547e8c1f3c98fdfdb630e4dacd2a699cc" - integrity sha512-FWo+xOlZ+BOzk8ZjyVvHB41wD4CoS/iHpCWPrKzghQiAE/Z/npCvz4DJ1hbzFhEbEkrv4TAikqHrvz5dTt9/jQ== - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.npmmirror.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -caniuse-lite@^1.0.30001759: - version "1.0.30001761" - resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz#4ca4c6e3792b24e8e2214baa568fc0e43de28191" - integrity sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g== - -chalk@^4.1.1: - version "4.1.2" - resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -chrome-trace-event@^1.0.2: - version "1.0.4" - resolved "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" - integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== - -chromium-pickle-js@^0.2.0: - version "0.2.0" - resolved "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" - integrity sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw== - -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone-response@^1.0.2: - version "1.0.3" - resolved "https://registry.npmmirror.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" - integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== - dependencies: - mimic-response "^1.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.14: - version "2.0.20" - resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -colors@1.0.3: - version "1.0.3" - resolved "https://registry.npmmirror.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@2.9.0: - version "2.9.0" - resolved "https://registry.npmmirror.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A== - dependencies: - graceful-readlink ">= 1.0.0" - -commander@^10.0.1: - version "10.0.1" - resolved "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^5.0.0: - version "5.1.0" - resolved "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -compare-version@^0.1.2: - version "0.1.2" - resolved "https://registry.npmmirror.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" - integrity sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -copy-anything@^2.0.1: - version "2.0.6" - resolved "https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" - integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw== - dependencies: - is-what "^3.14.1" - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -crc@^3.8.0: - version "3.8.0" - resolved "https://registry.npmmirror.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" - integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== - dependencies: - buffer "^5.1.0" - -cross-spawn-windows-exe@^1.1.0, cross-spawn-windows-exe@^1.2.0: - version "1.2.0" - resolved "https://registry.npmmirror.com/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz#46253b0f497676e766faf4a7061004618b5ac5ec" - integrity sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw== - dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - is-wsl "^2.2.0" - which "^2.0.2" - -cross-spawn@^7.0.1, cross-spawn@^7.0.3: - version "7.0.6" - resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: - version "4.4.3" - resolved "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -debug@^2.2.0, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.npmmirror.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== - dependencies: - mimic-response "^1.0.0" - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-data-property@^1.0.1: - version "1.1.4" - resolved "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -dir-compare@^2.4.0: - version "2.4.0" - resolved "https://registry.npmmirror.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" - integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== - dependencies: - buffer-equal "1.0.0" - colors "1.0.3" - commander "2.9.0" - minimatch "3.0.4" - -dir-compare@^3.0.0: - version "3.3.0" - resolved "https://registry.npmmirror.com/dir-compare/-/dir-compare-3.3.0.tgz#2c749f973b5c4b5d087f11edaae730db31788416" - integrity sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg== - dependencies: - buffer-equal "^1.0.0" - minimatch "^3.0.4" - -dmg-builder@23.6.0: - version "23.6.0" - resolved "https://registry.npmmirror.com/dmg-builder/-/dmg-builder-23.6.0.tgz#d39d3871bce996f16c07d2cafe922d6ecbb2a948" - integrity sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA== - dependencies: - app-builder-lib "23.6.0" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - fs-extra "^10.0.0" - iconv-lite "^0.6.2" - js-yaml "^4.1.0" - optionalDependencies: - dmg-license "^1.0.11" - -dmg-license@^1.0.11: - version "1.0.11" - resolved "https://registry.npmmirror.com/dmg-license/-/dmg-license-1.0.11.tgz#7b3bc3745d1b52be7506b4ee80cb61df6e4cd79a" - integrity sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q== - dependencies: - "@types/plist" "^3.0.1" - "@types/verror" "^1.10.3" - ajv "^6.10.0" - crc "^3.8.0" - iconv-corefoundation "^1.1.7" - plist "^3.0.4" - smart-buffer "^4.0.2" - verror "^1.10.0" - -dotenv-expand@^5.1.0: - version "5.1.0" - resolved "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" - integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== - -dotenv@^9.0.2: - version "9.0.2" - resolved "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" - integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -duplexer3@^0.1.4: - version "0.1.5" - resolved "https://registry.npmmirror.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" - integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== - -duplexer@^0.1.1, duplexer@~0.1.1: - version "0.1.2" - resolved "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -ejs@^3.1.7: - version "3.1.10" - resolved "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" - integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== - dependencies: - jake "^10.8.5" - -electron-builder@^23.3.3: - version "23.6.0" - resolved "https://registry.npmmirror.com/electron-builder/-/electron-builder-23.6.0.tgz#c79050cbdce90ed96c5feb67c34e9e0a21b5331b" - integrity sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw== - dependencies: - "@types/yargs" "^17.0.1" - app-builder-lib "23.6.0" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - chalk "^4.1.1" - dmg-builder "23.6.0" - fs-extra "^10.0.0" - is-ci "^3.0.0" - lazy-val "^1.0.5" - read-config-file "6.2.0" - simple-update-notifier "^1.0.7" - yargs "^17.5.1" - -electron-notarize@^1.1.1: - version "1.2.2" - resolved "https://registry.npmmirror.com/electron-notarize/-/electron-notarize-1.2.2.tgz#ebf2b258e8e08c1c9f8ff61dc53d5b16b439daf4" - integrity sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw== - dependencies: - debug "^4.1.1" - fs-extra "^9.0.1" - -electron-osx-sign@^0.5.0: - version "0.5.0" - resolved "https://registry.npmmirror.com/electron-osx-sign/-/electron-osx-sign-0.5.0.tgz#fc258c5e896859904bbe3d01da06902c04b51c3a" - integrity sha512-icoRLHzFz/qxzDh/N4Pi2z4yVHurlsCAYQvsCSG7fCedJ4UJXBS6PoQyGH71IfcqKupcKeK7HX/NkyfG+v6vlQ== - dependencies: - bluebird "^3.5.0" - compare-version "^0.1.2" - debug "^2.6.8" - isbinaryfile "^3.0.2" - minimist "^1.2.0" - plist "^3.0.1" - -electron-osx-sign@^0.6.0: - version "0.6.0" - resolved "https://registry.npmmirror.com/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz#9b69c191d471d9458ef5b1e4fdd52baa059f1bb8" - integrity sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg== - dependencies: - bluebird "^3.5.0" - compare-version "^0.1.2" - debug "^2.6.8" - isbinaryfile "^3.0.2" - minimist "^1.2.0" - plist "^3.0.1" - -electron-packager@^15.5.2: - version "15.5.2" - resolved "https://registry.npmmirror.com/electron-packager/-/electron-packager-15.5.2.tgz#9864796b8af63aa87b43151d0b6990ba7093e6b8" - integrity sha512-8zUdkSONn0jomu/efqoxApGzgqIb56ooMs671HeB/BXTPnWcWvqpEY08g16PL6ok2ymA5zPj8vmUszLrq99F0Q== - dependencies: - "@electron/get" "^1.6.0" - "@electron/universal" "^1.2.1" - asar "^3.1.0" - cross-spawn-windows-exe "^1.2.0" - debug "^4.0.1" - electron-notarize "^1.1.1" - electron-osx-sign "^0.5.0" - extract-zip "^2.0.0" - filenamify "^4.1.0" - fs-extra "^10.1.0" - galactus "^0.2.1" - get-package-info "^1.0.0" - junk "^3.1.0" - parse-author "^2.0.0" - plist "^3.0.0" - rcedit "^3.0.1" - resolve "^1.1.6" - semver "^7.1.3" - yargs-parser "^20.2.9" - -electron-publish@23.6.0: - version "23.6.0" - resolved "https://registry.npmmirror.com/electron-publish/-/electron-publish-23.6.0.tgz#ac9b469e0b07752eb89357dd660e5fb10b3d1ce9" - integrity sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg== - dependencies: - "@types/fs-extra" "^9.0.11" - builder-util "23.6.0" - builder-util-runtime "9.1.1" - chalk "^4.1.1" - fs-extra "^10.0.0" - lazy-val "^1.0.5" - mime "^2.5.2" - -electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== - -electron@^15.2.0: - version "15.5.7" - resolved "https://registry.npmmirror.com/electron/-/electron-15.5.7.tgz#aadb0081c504f2c2d8f81ea5fd23e38881afe86a" - integrity sha512-n4mVlxoMc4eYx07wWFWGficL+iOMz5xZEf5dBtE/wwLm0fQpYVyW4AlknMFG9F8Css0MM0JSwNMOyRg5e1vDtg== - dependencies: - "@electron/get" "^1.13.0" - "@types/node" "^14.6.2" - extract-zip "^1.0.3" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -encodeurl@^1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -end-of-stream@^1.1.0: - version "1.4.5" - resolved "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" - integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.17.4: - version "5.18.4" - resolved "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz#c22d33055f3952035ce6a144ce092447c525f828" - integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -envinfo@^7.7.3: - version "7.21.0" - resolved "https://registry.npmmirror.com/envinfo/-/envinfo-7.21.0.tgz#04a251be79f92548541f37d13c8b6f22940c3bae" - integrity sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow== - -errno@^0.1.1: - version "0.1.8" - resolved "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0: - version "1.3.4" - resolved "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" - integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== - dependencies: - is-arrayish "^0.2.1" - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-module-lexer@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1" - integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -es6-error@^4.1.1: - version "4.1.1" - resolved "https://registry.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" - integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== - -escalade@^3.1.1, escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -event-stream@^4.0.1: - version "4.0.1" - resolved "https://registry.npmmirror.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65" - integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA== - dependencies: - duplexer "^0.1.1" - from "^0.1.7" - map-stream "0.0.7" - pause-stream "^0.0.11" - split "^1.0.1" - stream-combiner "^0.2.2" - through "^2.3.8" - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -extract-zip@^1.0.3: - version "1.7.0" - resolved "https://registry.npmmirror.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extract-zip@^2.0.0: - version "2.0.1" - resolved "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-uri@^3.0.1: - version "3.1.0" - resolved "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" - integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== - -fastest-levenshtein@^1.0.12: - version "1.0.16" - resolved "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" - integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - -filelist@^1.0.4: - version "1.0.4" - resolved "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== - dependencies: - minimatch "^5.0.1" - -filename-reserved-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" - integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== - -filenamify@^4.1.0: - version "4.3.0" - resolved "https://registry.npmmirror.com/filenamify/-/filenamify-4.3.0.tgz#62391cb58f02b09971c9d4f9d63b3cf9aba03106" - integrity sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg== - dependencies: - filename-reserved-regex "^2.0.0" - strip-outer "^1.0.1" - trim-repeated "^1.0.0" - -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== - dependencies: - locate-path "^2.0.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.npmmirror.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flora-colossus@^1.0.0: - version "1.0.1" - resolved "https://registry.npmmirror.com/flora-colossus/-/flora-colossus-1.0.1.tgz#aba198425a8185341e64f9d2a6a96fd9a3cbdb93" - integrity sha512-d+9na7t9FyH8gBJoNDSi28mE4NgQVGGvxQ4aHtFRetjyh5SXjuus+V5EZaxFmFdXVemSOrx0lsgEl/ZMjnOWJA== - dependencies: - debug "^4.1.1" - fs-extra "^7.0.0" - -font-list@^1.4.2: - version "1.6.0" - resolved "https://registry.npmmirror.com/font-list/-/font-list-1.6.0.tgz#555d8e8b74a9a16d36fbbb96e43a1669284ba730" - integrity sha512-gI6N38fcq4er2BkKeW33SQt5nLMGX/GuT7mgWa3OiOZSQAJ3b6ZiCg3hPqlnnaULW/pcR1R7sUMrN6HfovXKAg== - -form-data@^4.0.0: - version "4.0.5" - resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" - integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.2" - mime-types "^2.1.12" - -from@^0.1.7: - version "0.1.7" - resolved "https://registry.npmmirror.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== - -fs-extra@^10.0.0, fs-extra@^10.1.0: - version "10.1.0" - resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^4.0.0: - version "4.0.3" - resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" - integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^7.0.0: - version "7.0.1" - resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^9.0.0, fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -galactus@^0.2.1: - version "0.2.1" - resolved "https://registry.npmmirror.com/galactus/-/galactus-0.2.1.tgz#cbed2d20a40c1f5679a35908e2b9415733e78db9" - integrity sha512-mDc8EQJKtxjp9PMYS3PbpjjbX3oXhBTxoGaPahw620XZBIHJ4+nvw5KN/tRtmmSDR9dypstGNvqQ3C29QGoGHQ== - dependencies: - debug "^3.1.0" - flora-colossus "^1.0.0" - fs-extra "^4.0.0" - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-package-info@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/get-package-info/-/get-package-info-1.0.0.tgz#6432796563e28113cd9474dbbd00052985a4999c" - integrity sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw== - dependencies: - bluebird "^3.1.1" - debug "^2.2.0" - lodash.get "^4.0.0" - read-pkg-up "^2.0.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmmirror.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^7.1.6: - version "7.2.3" - resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" - integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== - dependencies: - boolean "^3.0.1" - es6-error "^4.1.1" - matcher "^3.0.0" - roarr "^2.15.3" - semver "^7.3.2" - serialize-error "^7.0.1" - -global-tunnel-ng@^2.7.1: - version "2.7.1" - resolved "https://registry.npmmirror.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" - integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== - dependencies: - encodeurl "^1.0.2" - lodash "^4.17.10" - npm-conf "^1.1.3" - tunnel "^0.0.6" - -globalthis@^1.0.1: - version "1.0.4" - resolved "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.npmmirror.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4: - version "4.2.11" - resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.npmmirror.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.2" - resolved "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -hosted-git-info@^4.1.0: - version "4.1.0" - resolved "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" - integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== - dependencies: - lru-cache "^6.0.0" - -http-cache-semantics@^4.0.0: - version "4.2.0" - resolved "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" - integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -iconv-corefoundation@^1.1.7: - version "1.1.7" - resolved "https://registry.npmmirror.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a" - integrity sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ== - dependencies: - cli-truncate "^2.1.0" - node-addon-api "^1.6.3" - -iconv-lite@^0.6.2, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ieee754@^1.1.13, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -image-size@~0.5.0: - version "0.5.5" - resolved "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== - -import-local@^3.0.2: - version "3.2.0" - resolved "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" - integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -interpret@^3.1.1: - version "3.1.1" - resolved "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" - integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.npmmirror.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-core-module@^2.16.1: - version "2.16.1" - resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" - integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== - dependencies: - hasown "^2.0.2" - -is-docker@^2.0.0: - version "2.2.1" - resolved "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-what@^3.14.1: - version "3.14.1" - resolved "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" - integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isbinaryfile@^3.0.2: - version "3.0.3" - resolved "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80" - integrity sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw== - dependencies: - buffer-alloc "^1.2.0" - -isbinaryfile@^4.0.10: - version "4.0.10" - resolved "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" - integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jake@^10.8.5: - version "10.9.4" - resolved "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" - integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA== - dependencies: - async "^3.2.6" - filelist "^1.0.4" - picocolors "^1.1.1" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jr-qrcode@^1.1.4: - version "1.2.1" - resolved "https://registry.npmmirror.com/jr-qrcode/-/jr-qrcode-1.2.1.tgz#063f04720bf33bea135c40777dc8ab8d26e213f8" - integrity sha512-RXv5g2bIiLtkYDa6EcrE2gJC7dvLohNygrsrQ7+2UizBXeQKZ2UQd8owf0OHt+UXt6CFEg0xABxVf2axErzHlA== - -js-yaml@^4.1.0: - version "4.1.1" - resolved "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" - integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== - dependencies: - argparse "^2.0.1" - -jsbarcode@^3.11.5: - version "3.12.1" - resolved "https://registry.npmmirror.com/jsbarcode/-/jsbarcode-3.12.1.tgz#9ee84826efd8112e017aaf00f72217eae81823b9" - integrity sha512-QZQSqIknC2Rr/YOUyOkCBqsoiBAOTYK+7yNN3JsqfoUtJtkazxNw1dmPpxuv7VVvqW13kA3/mKiLq+s/e3o9hQ== - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== - -json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -json5@^2.2.0: - version "2.2.3" - resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^6.0.1: - version "6.2.0" - resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" - integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -junk@^3.1.0: - version "3.1.0" - resolved "https://registry.npmmirror.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" - integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.npmmirror.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -lazy-val@^1.0.4, lazy-val@^1.0.5: - version "1.0.5" - resolved "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d" - integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q== - -less-loader@^10.2.0: - version "10.2.0" - resolved "https://registry.npmmirror.com/less-loader/-/less-loader-10.2.0.tgz#97286d8797dc3dc05b1d16b0ecec5f968bdd4e32" - integrity sha512-AV5KHWvCezW27GT90WATaDnfXBv99llDbtaj4bshq6DvAihMdNjaPDcUMa6EXKLRF+P2opFenJp89BXg91XLYg== - dependencies: - klona "^2.0.4" - -less@^4.1.2: - version "4.5.1" - resolved "https://registry.npmmirror.com/less/-/less-4.5.1.tgz#739266532249a3de232e8b60ffb1b27ad5ec6ad8" - integrity sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw== - dependencies: - copy-anything "^2.0.1" - parse-node-version "^1.0.1" - tslib "^2.3.0" - optionalDependencies: - errno "^0.1.1" - graceful-fs "^4.1.2" - image-size "~0.5.0" - make-dir "^2.1.0" - mime "^1.4.1" - needle "^3.1.0" - source-map "~0.6.0" - -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ== - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -loader-runner@^4.3.1: - version "4.3.1" - resolved "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" - integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.npmmirror.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - integrity sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw== - -lodash.get@^4.0.0: - version "4.4.2" - resolved "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash@^4.17.10, lodash@^4.17.15: - version "4.17.21" - resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -map-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmmirror.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" - integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ== - -matcher@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" - integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== - dependencies: - escape-string-regexp "^4.0.0" - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@^2.1.27: - version "2.1.35" - resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@^1.4.1: - version "1.6.0" - resolved "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.5.2: - version "2.6.0" - resolved "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mingo@^6.1.0: - version "6.7.2" - resolved "https://registry.npmmirror.com/mingo/-/mingo-6.7.2.tgz#cf0f619ad0b03870cedabad6cc828bbd20c49106" - integrity sha512-5HKLu2GiNxw71EYZbWPh6dpcucHLlHqOXa0gbsYYtAjRpT/8EbDBJURLlUt3cNkISd9grOJ/AeYogyWj3QoxGA== - -minimatch@*: - version "10.1.1" - resolved "https://registry.npmmirror.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" - integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== - dependencies: - "@isaacs/brace-expansion" "^5.0.0" - -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass@^3.0.0: - version "3.3.6" - resolved "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp@^0.5.4: - version "0.5.6" - resolved "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@^2.1.1, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -needle@^3.1.0: - version "3.3.1" - resolved "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz#63f75aec580c2e77e209f3f324e2cdf3d29bd049" - integrity sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q== - dependencies: - iconv-lite "^0.6.3" - sax "^1.2.4" - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -node-addon-api@^1.6.3: - version "1.7.2" - resolved "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" - integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== - -node-releases@^2.0.27: - version "2.0.27" - resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" - integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.npmmirror.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -npm-conf@^1.1.3: - version "1.1.3" - resolved "https://registry.npmmirror.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" - integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== - dependencies: - config-chain "^1.1.11" - pify "^3.0.0" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmmirror.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.npmmirror.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.npmmirror.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== - dependencies: - p-limit "^1.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parse-author@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/parse-author/-/parse-author-2.0.0.tgz#d3460bf1ddd0dfaeed42da754242e65fb684a81f" - integrity sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw== - dependencies: - author-regex "^1.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.npmmirror.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== - dependencies: - error-ex "^1.2.0" - -parse-node-version@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ== - dependencies: - pify "^2.0.0" - -pause-stream@^0.0.11: - version "0.0.11" - resolved "https://registry.npmmirror.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== - dependencies: - through "~2.3" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -plist@^3.0.0, plist@^3.0.1, plist@^3.0.4: - version "3.1.0" - resolved "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" - integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== - dependencies: - "@xmldom/xmldom" "^0.8.8" - base64-js "^1.5.1" - xmlbuilder "^15.1.1" - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== - -pump@^3.0.0: - version "3.0.3" - resolved "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" - integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -rcedit@^3.0.1: - version "3.1.0" - resolved "https://registry.npmmirror.com/rcedit/-/rcedit-3.1.0.tgz#1563ec7a5663de639f94c5dc85429db1da364b3e" - integrity sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA== - dependencies: - cross-spawn-windows-exe "^1.1.0" - -read-config-file@6.2.0: - version "6.2.0" - resolved "https://registry.npmmirror.com/read-config-file/-/read-config-file-6.2.0.tgz#71536072330bcd62ba814f91458b12add9fc7ade" - integrity sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg== - dependencies: - dotenv "^9.0.2" - dotenv-expand "^5.1.0" - js-yaml "^4.1.0" - json5 "^2.2.0" - lazy-val "^1.0.4" - -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w== - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA== - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -readable-stream@^2.2.2: - version "2.3.8" - resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -rechoir@^0.8.0: - version "0.8.0" - resolved "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" - integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== - dependencies: - resolve "^1.20.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.20.0: - version "1.22.11" - resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" - integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== - dependencies: - is-core-module "^2.16.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== - dependencies: - lowercase-keys "^1.0.0" - -roarr@^2.15.3: - version "2.15.4" - resolved "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" - integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== - dependencies: - boolean "^3.0.1" - detect-node "^2.0.4" - globalthis "^1.0.1" - json-stringify-safe "^5.0.1" - semver-compare "^1.0.0" - sprintf-js "^1.1.2" - -safe-buffer@^5.1.0: - version "5.2.1" - resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -"safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sanitize-filename@^1.6.3: - version "1.6.3" - resolved "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" - integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== - dependencies: - truncate-utf8-bytes "^1.0.0" - -save@^2.9.0: - version "2.9.0" - resolved "https://registry.npmmirror.com/save/-/save-2.9.0.tgz#6659375fadeaf58e4abc6a90ec6b0fb2e2232e0f" - integrity sha512-eg8+g8CjvehE/2C6EbLdtK1pINVD27pcJLj4M9PjWWhoeha/y5bWf4dp/0RF+OzbKTcG1bae9qi3PAqiR8CJTg== - dependencies: - async "^3.2.2" - event-stream "^4.0.1" - lodash.assign "^4.2.0" - mingo "^6.1.0" - -sax@^1.2.4: - version "1.4.3" - resolved "https://registry.npmmirror.com/sax/-/sax-1.4.3.tgz#fcebae3b756cdc8428321805f4b70f16ec0ab5db" - integrity sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ== - -schema-utils@^4.3.0, schema-utils@^4.3.3: - version "4.3.3" - resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" - integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== - -"semver@2 || 3 || 4 || 5", semver@^5.6.0: - version "5.7.2" - resolved "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -semver@^6.2.0: - version "6.3.1" - resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.1.3, semver@^7.3.2, semver@^7.3.7: - version "7.7.3" - resolved "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== - -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.npmmirror.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -serialize-error@^7.0.1: - version "7.0.1" - resolved "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" - integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== - dependencies: - type-fest "^0.13.1" - -serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -simple-update-notifier@^1.0.7: - version "1.1.0" - resolved "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" - integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== - dependencies: - semver "~7.0.0" - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -smart-buffer@^4.0.2: - version "4.2.0" - resolved "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -source-map-support@^0.5.19, source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.5.0" - resolved "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" - integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.22" - resolved "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz#abf5a08a6f5d7279559b669f47f0a43e8f3464ef" - integrity sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ== - -split@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -sprintf-js@^1.1.2: - version "1.1.3" - resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - -stat-mode@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" - integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== - -stream-combiner@^0.2.2: - version "0.2.2" - resolved "https://registry.npmmirror.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" - integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ== - dependencies: - duplexer "~0.1.1" - through "~2.3.4" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-outer@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" - integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== - dependencies: - escape-string-regexp "^1.0.2" - -sumchecker@^3.0.1: - version "3.0.1" - resolved "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" - integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== - dependencies: - debug "^4.1.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -tapable@^2.2.0, tapable@^2.3.0: - version "2.3.0" - resolved "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" - integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== - -tar@^6.1.11: - version "6.2.1" - resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -temp-file@^3.4.0: - version "3.4.0" - resolved "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz#766ea28911c683996c248ef1a20eea04d51652c7" - integrity sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg== - dependencies: - async-exit-hook "^2.0.1" - fs-extra "^10.0.0" - -terser-webpack-plugin@^5.3.16: - version "5.3.16" - resolved "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz#741e448cc3f93d8026ebe4f7ef9e4afacfd56330" - integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q== - dependencies: - "@jridgewell/trace-mapping" "^0.3.25" - jest-worker "^27.4.5" - schema-utils "^4.3.0" - serialize-javascript "^6.0.2" - terser "^5.31.1" - -terser@^5.31.1: - version "5.44.1" - resolved "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c" - integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.15.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -through@2, through@^2.3.8, through@~2.3, through@~2.3.4: - version "2.3.8" - resolved "https://registry.npmmirror.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -tmp-promise@^3.0.2: - version "3.0.3" - resolved "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" - integrity sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ== - dependencies: - tmp "^0.2.0" - -tmp@^0.2.0: - version "0.2.5" - resolved "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" - integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -trim-repeated@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" - integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== - dependencies: - escape-string-regexp "^1.0.2" - -truncate-utf8-bytes@^1.0.0: - version "1.0.2" - resolved "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" - integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ== - dependencies: - utf8-byte-length "^1.0.1" - -tslib@^2.3.0: - version "2.8.1" - resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -tunnel@^0.0.6: - version "0.0.6" - resolved "https://registry.npmmirror.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" - integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== - -type-fest@^0.13.1: - version "0.13.1" - resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" - integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== - -undici-types@~7.16.0: - version "7.16.0" - resolved "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" - integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -update-browserslist-db@^1.2.0: - version "1.2.3" - resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" - integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== - dependencies: - prepend-http "^2.0.0" - -utf8-byte-length@^1.0.1: - version "1.0.5" - resolved "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e" - integrity sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA== - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -verror@^1.10.0: - version "1.10.1" - resolved "https://registry.npmmirror.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb" - integrity sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -watchpack@^2.4.4: - version "2.4.4" - resolved "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" - integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -webpack-cli@^5.1.4: - version "5.1.4" - resolved "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" - integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== - dependencies: - "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^2.1.1" - "@webpack-cli/info" "^2.0.2" - "@webpack-cli/serve" "^2.0.5" - colorette "^2.0.14" - commander "^10.0.1" - cross-spawn "^7.0.3" - envinfo "^7.7.3" - fastest-levenshtein "^1.0.12" - import-local "^3.0.2" - interpret "^3.1.1" - rechoir "^0.8.0" - webpack-merge "^5.7.3" - -webpack-merge@^5.7.3: - version "5.10.0" - resolved "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" - integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== - dependencies: - clone-deep "^4.0.1" - flat "^5.0.2" - wildcard "^2.0.0" - -webpack-sources@^3.3.3: - version "3.3.3" - resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" - integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== - -webpack@^5.96.1: - version "5.104.1" - resolved "https://registry.npmmirror.com/webpack/-/webpack-5.104.1.tgz#94bd41eb5dbf06e93be165ba8be41b8260d4fb1a" - integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA== - dependencies: - "@types/eslint-scope" "^3.7.7" - "@types/estree" "^1.0.8" - "@types/json-schema" "^7.0.15" - "@webassemblyjs/ast" "^1.14.1" - "@webassemblyjs/wasm-edit" "^1.14.1" - "@webassemblyjs/wasm-parser" "^1.14.1" - acorn "^8.15.0" - acorn-import-phases "^1.0.3" - browserslist "^4.28.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.4" - es-module-lexer "^2.0.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.11" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.3.1" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^4.3.3" - tapable "^2.3.0" - terser-webpack-plugin "^5.3.16" - watchpack "^2.4.4" - webpack-sources "^3.3.3" - -which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wildcard@^2.0.0: - version "2.0.1" - resolved "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" - integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1: - version "15.1.1" - resolved "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" - integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yargs-parser@^20.2.9: - version "20.2.9" - resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.5.1: - version "17.7.2" - resolved "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0"